@gram-ai/create-function 0.3.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,6 +2,8 @@ All done! Jump in with `cd $DIR`.
2
2
 
3
3
  Some next steps:
4
4
 
5
+ - Start a local development MCP server with `$PACKAGE_MANAGER run dev`
6
+
5
7
  - Ensure the `Gram CLI` is installed and authenticated (`gram auth`): https://docs.getgram.ai/command-line/installation
6
8
 
7
9
  - Build your function with `$PACKAGE_MANAGER run build`
@@ -40,20 +40,16 @@ pnpm build
40
40
  ## Testing Locally
41
41
 
42
42
  If you want to poke at the tools you've built during local development, you can
43
- start a Hono server with:
43
+ start a local MCP server over stdio transport with:
44
44
 
45
45
  ```bash
46
46
  pnpm dev
47
47
  ```
48
48
 
49
- Now you can simulate tool calls with:
49
+ Specifically, this command will spin up [MCP inspector][mcp-inspector] to let
50
+ you interactively test your tools.
50
51
 
51
- ```bash
52
- curl \
53
- --data '{"name": "greet", "input": {"name": "Georges"}}' \
54
- -H "Content-Type: application/json" \
55
- http://localhost:3000/tool-call
56
- ```
52
+ [mcp-inspector]: https://github.com/modelcontextprotocol/inspector
57
53
 
58
54
  ## What next?
59
55
 
@@ -0,0 +1,21 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*|*MINGW*|*MSYS*)
6
+ if command -v cygpath > /dev/null 2>&1; then
7
+ basedir=`cygpath -w "$basedir"`
8
+ fi
9
+ ;;
10
+ esac
11
+
12
+ if [ -z "$NODE_PATH" ]; then
13
+ export NODE_PATH="/home/runner/work/gram/gram/node_modules/.pnpm/@modelcontextprotocol+inspector@0.17.1_@types+node@22.18.6_@types+react-dom@19.1.9_@typ_cc52197392af7d7a50d2088d86700a7d/node_modules/@modelcontextprotocol/inspector/cli/build/node_modules:/home/runner/work/gram/gram/node_modules/.pnpm/@modelcontextprotocol+inspector@0.17.1_@types+node@22.18.6_@types+react-dom@19.1.9_@typ_cc52197392af7d7a50d2088d86700a7d/node_modules/@modelcontextprotocol/inspector/cli/node_modules:/home/runner/work/gram/gram/node_modules/.pnpm/@modelcontextprotocol+inspector@0.17.1_@types+node@22.18.6_@types+react-dom@19.1.9_@typ_cc52197392af7d7a50d2088d86700a7d/node_modules/@modelcontextprotocol/inspector/node_modules:/home/runner/work/gram/gram/node_modules/.pnpm/@modelcontextprotocol+inspector@0.17.1_@types+node@22.18.6_@types+react-dom@19.1.9_@typ_cc52197392af7d7a50d2088d86700a7d/node_modules/@modelcontextprotocol/node_modules:/home/runner/work/gram/gram/node_modules/.pnpm/@modelcontextprotocol+inspector@0.17.1_@types+node@22.18.6_@types+react-dom@19.1.9_@typ_cc52197392af7d7a50d2088d86700a7d/node_modules:/home/runner/work/gram/gram/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/home/runner/work/gram/gram/node_modules/.pnpm/@modelcontextprotocol+inspector@0.17.1_@types+node@22.18.6_@types+react-dom@19.1.9_@typ_cc52197392af7d7a50d2088d86700a7d/node_modules/@modelcontextprotocol/inspector/cli/build/node_modules:/home/runner/work/gram/gram/node_modules/.pnpm/@modelcontextprotocol+inspector@0.17.1_@types+node@22.18.6_@types+react-dom@19.1.9_@typ_cc52197392af7d7a50d2088d86700a7d/node_modules/@modelcontextprotocol/inspector/cli/node_modules:/home/runner/work/gram/gram/node_modules/.pnpm/@modelcontextprotocol+inspector@0.17.1_@types+node@22.18.6_@types+react-dom@19.1.9_@typ_cc52197392af7d7a50d2088d86700a7d/node_modules/@modelcontextprotocol/inspector/node_modules:/home/runner/work/gram/gram/node_modules/.pnpm/@modelcontextprotocol+inspector@0.17.1_@types+node@22.18.6_@types+react-dom@19.1.9_@typ_cc52197392af7d7a50d2088d86700a7d/node_modules/@modelcontextprotocol/node_modules:/home/runner/work/gram/gram/node_modules/.pnpm/@modelcontextprotocol+inspector@0.17.1_@types+node@22.18.6_@types+react-dom@19.1.9_@typ_cc52197392af7d7a50d2088d86700a7d/node_modules:/home/runner/work/gram/gram/node_modules/.pnpm/node_modules:$NODE_PATH"
16
+ fi
17
+ if [ -x "$basedir/node" ]; then
18
+ exec "$basedir/node" "$basedir/../@modelcontextprotocol/inspector/cli/build/cli.js" "$@"
19
+ else
20
+ exec node "$basedir/../@modelcontextprotocol/inspector/cli/build/cli.js" "$@"
21
+ fi
@@ -19,7 +19,7 @@
19
19
  "node": ">=22.18.0"
20
20
  },
21
21
  "scripts": {
22
- "dev": "node ./src/server.ts",
22
+ "dev": "mcp-inspector node ./src/server.ts",
23
23
  "lint": "tsc --noEmit",
24
24
  "build": "node -e \"console.log('Stubbed out')\"",
25
25
  "_:build": "gram-build",
@@ -28,12 +28,12 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@gram-ai/functions": "workspace:",
31
+ "@modelcontextprotocol/sdk": "^1.20.1",
31
32
  "zod": "^4"
32
33
  },
33
34
  "devDependencies": {
34
- "@hono/node-server": "^1.19.5",
35
+ "@modelcontextprotocol/inspector": "^0.17.1",
35
36
  "@types/node": "22.x",
36
- "hono": "^4",
37
37
  "typescript": "^5"
38
38
  }
39
39
  }
@@ -1,44 +1,125 @@
1
- import { serve } from "@hono/node-server";
2
- import { Hono } from "hono";
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import {
3
+ CallToolRequestSchema,
4
+ ListToolsRequestSchema,
5
+ type CallToolResult,
6
+ type ListToolsResult,
7
+ } from "@modelcontextprotocol/sdk/types.js";
8
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+
10
+ import pkg from "../package.json" with { type: "json" };
3
11
  import { gram } from "./gram.ts";
4
12
 
5
- const app = new Hono();
6
-
7
- app.post("/tool-call", async (c) => {
8
- const req = await c.req.json();
9
- return await gram.handleToolCall(req, {
10
- signal: AbortSignal.timeout(1 * 60 * 1000),
11
- });
12
- });
13
-
14
- app.get("/manifest", async () => {
15
- const manifest = gram.manifest();
16
- return new Response(JSON.stringify(manifest, null, 2), {
17
- headers: { "Content-Type": "application/json" },
18
- });
19
- });
20
-
21
- export function startServer(port: number = 3000) {
22
- const server = serve({ fetch: app.fetch, port }, (info) => {
23
- console.log(`Listening on http://localhost:${info.port}`);
24
- });
25
-
26
- const onClose = () => {
27
- console.log("Shutting down server...");
28
- server.close((err) => {
29
- if (err) {
30
- console.error(err);
31
- process.exit(1);
32
- }
33
- process.exit(0);
13
+ const structuredLike = /\b(yaml|yml|json|toml|xml|xhtml)\b/i;
14
+ const textLike = /^text\//i;
15
+ const imageLike = /^image\//i;
16
+ const audioLike = /^audio\//i;
17
+
18
+ export const server = new Server(
19
+ {
20
+ name: pkg.name,
21
+ version: pkg.version,
22
+ },
23
+ {
24
+ capabilities: {
25
+ tools: {},
26
+ },
27
+ },
28
+ );
29
+
30
+ server.setRequestHandler(
31
+ ListToolsRequestSchema,
32
+ async (): Promise<ListToolsResult> => {
33
+ const tools = gram.manifest().tools.map((t) => {
34
+ return {
35
+ name: t.name,
36
+ description: t.description,
37
+ inputSchema: t.inputSchema,
38
+ };
39
+ }) as ListToolsResult["tools"];
40
+
41
+ return {
42
+ tools,
43
+ };
44
+ },
45
+ );
46
+
47
+ server.setRequestHandler(
48
+ CallToolRequestSchema,
49
+ async (req, extra): Promise<CallToolResult> => {
50
+ const { name, arguments: args } = req.params;
51
+
52
+ const resp = await gram.handleToolCall({ name, input: args } as any, {
53
+ signal: extra.signal,
34
54
  });
55
+
56
+ let ctype = resp.headers.get("Content-Type") || "";
57
+ ctype = ctype.split(";")[0]?.trim() || "";
58
+
59
+ switch (true) {
60
+ case textLike.test(ctype) || structuredLike.test(ctype): {
61
+ const text = await resp.text();
62
+ return {
63
+ content: [{ type: "text", text }],
64
+ };
65
+ }
66
+ case imageLike.test(ctype): {
67
+ return {
68
+ content: [
69
+ {
70
+ type: "image",
71
+ mimeType: ctype,
72
+ data: await responseToBase64(resp),
73
+ },
74
+ ],
75
+ };
76
+ }
77
+ case audioLike.test(ctype): {
78
+ return {
79
+ content: [
80
+ {
81
+ type: "audio",
82
+ mimeType: ctype,
83
+ data: await responseToBase64(resp),
84
+ },
85
+ ],
86
+ };
87
+ }
88
+ default: {
89
+ return {
90
+ isError: true,
91
+ content: [
92
+ {
93
+ type: "text",
94
+ text: `Unhandled content type: ${ctype}. Create a handler for this type in the MCP server.`,
95
+ },
96
+ ],
97
+ };
98
+ }
99
+ }
100
+ },
101
+ );
102
+
103
+ async function responseToBase64(resp: Response): Promise<string> {
104
+ const blob = await resp.arrayBuffer();
105
+ const buffer = Buffer.from(blob);
106
+ return buffer.toString("base64");
107
+ }
108
+
109
+ async function run() {
110
+ console.error("Starting MCP server with stdio...");
111
+ const stdio = new StdioServerTransport();
112
+ await server.connect(stdio);
113
+
114
+ const quit = async () => {
115
+ console.error("\nShutting down MCP server...");
116
+ await server.close();
117
+ process.exit(0);
35
118
  };
36
- process.on("SIGINT", onClose);
37
- process.on("SIGTERM", onClose);
119
+ process.once("SIGINT", quit);
120
+ process.once("SIGTERM", quit);
38
121
  }
39
122
 
40
123
  if (import.meta.main) {
41
- startServer();
124
+ run();
42
125
  }
43
-
44
- export default app;
@@ -26,3 +26,20 @@ export async function handleToolCall(call: {
26
26
  headers: { "Content-Type": "application/json; mcp=tools_call" },
27
27
  });
28
28
  }
29
+
30
+ export async function handleResources(call: {
31
+ uri: string;
32
+ input: string;
33
+ _meta?: Record<string, unknown>;
34
+ }): Promise<Response> {
35
+ const response = await client.readResource({
36
+ uri: call.uri,
37
+ _meta: call._meta,
38
+ });
39
+
40
+ const body = JSON.stringify(response);
41
+ return new Response(body, {
42
+ status: 200,
43
+ headers: { "Content-Type": "application/json" },
44
+ });
45
+ }
@@ -20,3 +20,24 @@ server.registerTool(
20
20
  };
21
21
  },
22
22
  );
23
+
24
+ server.registerResource(
25
+ "a-cool-photo",
26
+ "resources://a-cool-photo",
27
+ {
28
+ mimeType: "image/jpg",
29
+ description: "This photo is really something",
30
+ title: "A Cool Photo",
31
+ },
32
+ async (uri) => {
33
+ let res = await fetch("https://picsum.photos/200/300.jpg");
34
+ return {
35
+ contents: [
36
+ {
37
+ uri: uri.href,
38
+ blob: Buffer.from(await res.arrayBuffer()).toString("base64"),
39
+ },
40
+ ],
41
+ };
42
+ },
43
+ );
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gram-ai/create-function",
4
- "version": "0.3.2",
4
+ "version": "0.5.0",
5
5
  "description": "Build AI tools and deploy them to getgram.ai",
6
6
  "keywords": [],
7
7
  "homepage": "https://github.com/speakeasy-api/gram",
@@ -38,7 +38,7 @@
38
38
  "prettier": "^3.6.2",
39
39
  "typescript": "5.9.2",
40
40
  "zod": "^3.25.76",
41
- "@gram-ai/functions": "^0.3.2"
41
+ "@gram-ai/functions": "^0.5.0"
42
42
  },
43
43
  "scripts": {
44
44
  "build": "tsc --noEmit false"