diff --git a/.changeset/wet-banks-join.md b/.changeset/wet-banks-join.md new file mode 100644 index 000000000..d42a3249d --- /dev/null +++ b/.changeset/wet-banks-join.md @@ -0,0 +1,5 @@ +--- +"@gram-ai/functions": minor +--- + +allow for defining resources in mcp builds of gram functions diff --git a/.mise-tasks/start/server.sh b/.mise-tasks/start/server.sh index 043697db0..aec8991cb 100755 --- a/.mise-tasks/start/server.sh +++ b/.mise-tasks/start/server.sh @@ -2,7 +2,6 @@ #MISE dir="{{ config_root }}/server" #MISE description="Start up the API server" -#MISE sources=["server/**/*.go"] GIT_SHA=$(git rev-parse HEAD) @@ -11,4 +10,4 @@ if [ -f "../config.local.toml" ]; then CONFIG_ARGS=(--config-file ../config.local.toml) fi -go run -ldflags="-X github.com/speakeasy-api/gram/server/cmd/gram.GitSHA=${GIT_SHA} -X goa.design/clue/health.Version=${GIT_SHA}" main.go start "${CONFIG_ARGS[@]}" "$@" \ No newline at end of file +go run -ldflags="-X github.com/speakeasy-api/gram/server/cmd/gram.GitSHA=${GIT_SHA} -X goa.design/clue/health.Version=${GIT_SHA}" main.go start "${CONFIG_ARGS[@]}" "$@" diff --git a/ts-framework/create-function/gram-template-mcp/src/functions.ts b/ts-framework/create-function/gram-template-mcp/src/functions.ts index eb0b4d9aa..821e8a60f 100644 --- a/ts-framework/create-function/gram-template-mcp/src/functions.ts +++ b/ts-framework/create-function/gram-template-mcp/src/functions.ts @@ -26,3 +26,20 @@ export async function handleToolCall(call: { headers: { "Content-Type": "application/json; mcp=tools_call" }, }); } + +export async function handleResources(call: { + uri: string; + input: string; + _meta?: Record; +}): Promise { + const response = await client.readResource({ + uri: call.uri, + _meta: call._meta, + }); + + const body = JSON.stringify(response); + return new Response(body, { + status: 200, + headers: { "Content-Type": "application/json" }, + }); +} diff --git a/ts-framework/create-function/gram-template-mcp/src/mcp.ts b/ts-framework/create-function/gram-template-mcp/src/mcp.ts index 5c7a88144..69cd1fb38 100644 --- a/ts-framework/create-function/gram-template-mcp/src/mcp.ts +++ b/ts-framework/create-function/gram-template-mcp/src/mcp.ts @@ -20,3 +20,24 @@ server.registerTool( }; }, ); + +server.registerResource( + "a-cool-photo", + "resources://a-cool-photo", + { + mimeType: "image/jpg", + description: "This photo is really something", + title: "A Cool Photo", + }, + async (uri) => { + let res = await fetch("https://picsum.photos/200/300.jpg"); + return { + contents: [ + { + uri: uri.href, + blob: Buffer.from(await res.arrayBuffer()).toString("base64"), + }, + ], + }; + }, +); diff --git a/ts-framework/functions/src/build/mcp.ts b/ts-framework/functions/src/build/mcp.ts index 00f03a5e1..5d7d6dfdf 100644 --- a/ts-framework/functions/src/build/mcp.ts +++ b/ts-framework/functions/src/build/mcp.ts @@ -2,6 +2,11 @@ import { writeFile, open, stat, mkdir } from "node:fs/promises"; import { join, resolve } from "node:path"; import esbuild from "esbuild"; import archiver from "archiver"; +import { + McpError, + ErrorCode as McpErrorCode, +} from "@modelcontextprotocol/sdk/types.js"; +import type { Client } from "@modelcontextprotocol/sdk/client"; export type BuildMCPServerResult = { files: Array<{ path: string; size: number }>; @@ -60,6 +65,14 @@ async function buildFunctionsManifest(options: { name: string; description?: string | undefined; inputSchema: unknown; + _meta?: unknown; + }>; + resources?: Array<{ + name: string; + description?: string | undefined; + uri: string; + mimeType?: string | undefined; + _meta?: unknown; }>; }> { const cwd = options.cwd ?? process.cwd(); @@ -92,16 +105,61 @@ async function buildFunctionsManifest(options: { await server.connect(serverTransport); await mcpClient.connect(clientTransport); - const res = await mcpClient.listTools(); - const tools = res.tools.map((tool) => { - return { - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema, - }; - }); + let tools = await collectTools(mcpClient); + let resources = await collectResources(mcpClient); + + return { version: "0.0.0", tools, resources }; +} + +async function collectTools(client: Client) { + try { + const res = await client.listTools(); + return res.tools.map((tool) => { + return { + name: tool.name, + description: tool.description, + inputSchema: tool.inputSchema, + meta: { + "gram.ai/kind": "mcp-passthrough", + ...tool._meta, + }, + }; + }); + } catch (err) { + if (err instanceof McpError && err.code === McpErrorCode.MethodNotFound) { + console.warn("No tools registered"); + } else { + throw err; + } + return []; + } +} + +async function collectResources(client: Client) { + try { + const resourcesResponse = await client.listResources(); + return resourcesResponse.resources.map((resource) => { + return { + name: resource.name, + description: resource.description, + uri: resource.uri, + mimeType: resource.mimeType, + title: resource.title, + meta: { + "gram.ai/kind": "mcp-passthrough", + ...resource._meta, + }, + }; + }); + } catch (err) { + if (err instanceof McpError && err.code === McpErrorCode.MethodNotFound) { + console.warn("No tools registered"); + } else { + throw err; + } + } - return { version: "0.0.0", tools }; + return []; } async function bundleFunction(options: {