Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/wet-banks-join.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@gram-ai/functions": minor
---

allow for defining resources in mcp builds of gram functions
3 changes: 1 addition & 2 deletions .mise-tasks/start/server.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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[@]}" "$@"
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[@]}" "$@"
17 changes: 17 additions & 0 deletions ts-framework/create-function/gram-template-mcp/src/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>;
}): Promise<Response> {
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" },
});
}
21 changes: 21 additions & 0 deletions ts-framework/create-function/gram-template-mcp/src/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
},
],
};
},
);
76 changes: 67 additions & 9 deletions ts-framework/functions/src/build/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }>;
Expand Down Expand Up @@ -60,6 +65,14 @@ async function buildFunctionsManifest(options: {
name: string;
description?: string | undefined;
inputSchema: unknown;
_meta?: unknown;
}>;
resources?: Array<{
name: string;
description?: string | undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think something we need to figure out. Although descriptions seemingly are optional at the MCP SDK level for tools and resources our Gram Functions system requires them. Not totally sure what to do about this

uri: string;
mimeType?: string | undefined;
_meta?: unknown;
}>;
}> {
const cwd = options.cwd ?? process.cwd();
Expand Down Expand Up @@ -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: {
Expand Down