Imported from speakeasy-api/gram (
examples/temporal/AGENTS.md). Install upstream withnpx skills add speakeasy-api/gram --skill temporal. Copyright stays with the author.
Project Contribution Guide
- This is a Node.js project using TypeScript to build Gram Functions.
- The codebase assumes Node.js v22 or later is in use and TypeScript v5.9 or later is required.
- When working in this codebase prefer using the Web APIs and Node.js standard library where possible instead of third-party libraries.
Project Structure
src/- Contains the source TypeScript code for the Gram Functions.dist/- Output directory for the built code (generated after runningnpm run build).package.json- Defines project metadata, dependencies, and scripts.tsconfig.json- TypeScript configuration file.
package.json scripts
dev- Runs the MCP Inspector playground with hot reloading enabled.build- Bundles the Gram Functions code into a zip file for deployment and places it in thedist/directory.lint- Runs the TypeScript compiler innoEmitmode to check for type errors.
Core Concepts
The Gram Instance
The Gram class is the main entry point for defining tools. You create an
instance and chain .tool() calls to register multiple tools:
import { Gram } from "@gram-ai/functions";
import * as z from "zod/mini";
const g = new Gram()
.tool({
name: "add",
description: "Add two numbers",
inputSchema: { a: z.number(), b: z.number() },
async execute(ctx, input) {
return ctx.json({ sum: input.a + input.b });
},
})
.tool({
name: "multiply",
description: "Multiply two numbers",
inputSchema: { a: z.number(), b: z.number() },
async execute(ctx, input) {
return ctx.json({ product: input.a * input.b });
},
});
export const handleToolCall = g.handleToolCall;
Tool Definition
Each tool requires:
- name: A unique identifier for the tool
- description (optional): Human-readable description of what the tool does
- inputSchema: A Zod schema object defining the expected input parameters
- execute: An async function that implements the tool logic
Tool Context
The execute function receives a ctx (context) object with helper methods:
ctx.json(data)
Returns a JSON response:
async execute(ctx, input) {
return ctx.json({ result: "success", value: 42 });
}
ctx.text(data)
Returns a plain text response:
async execute(ctx, input) {
return ctx.text("Operation completed successfully");
}
ctx.html(data)
Returns an HTML response:
async execute(ctx, input) {
return ctx.html("<h1>Hello, World!</h1>");
}
ctx.markdown(data)
Returns a Markdown response:
async execute(ctx, input) {
return ctx.markdown("# Heading");
}
ctx.fail(data, options?)
Throws an error response (never returns):
async execute(ctx, input) {
if (!input.value) {
ctx.fail({ error: "value is required" }, { status: 400 });
}
// ...
}
ctx.signal
An AbortSignal for handling cancellation:
async execute(ctx, input) {
const response = await fetch(input.url, { signal: ctx.signal });
return ctx.json(await response.json());
}
ctx.env
Access to parsed environment variables defined by the Gram instance:
const gram = new Gram({
envSchema: {
BASE_URL: z.string().transform((url) => new URL(url)),
},
}).tool({
name: "api_call",
inputSchema: { endpoint: z.string() },
async execute(ctx, input) {
const baseURL = ctx.env.BASE_URL;
// Use baseURL...
},
});
Input Validation
Input schemas are defined using Zod:
import * as z from "zod/mini";
.tool({
name: "create_user",
inputSchema: {
email: z.string().check(z.email()),
age: z.number().check(z.min(18)),
name: z.optional(z.string()),
},
async execute(ctx, input) {
// input is fully typed based on the schema
return ctx.json({ userId: "123" });
},
})
Lax Mode
By default, the framework strictly validates input. You can enable lax mode to allow unvalidated input to pass through:
const g = new Gram({ lax: true });
Environment Variables
Defining Variables
Environment variables that are used by tools must be defined when instantiating
the Gram class. This is done using a Zod v4 object schema:
import * as z from "zod/mini";
const gram = new Gram({
envSchema: {
API_KEY: z.string().describe("API key for external service"),
BASE_URL: z.string().check(z.url()).describe("Base URL for API requests"),
},
});
Whenever a tool wants to access a new environment variable, a definition must be
added to the envSchema if one does not exist. When this Gram Function is
deployed, end users will then be able to provide values for these variables when
installing the corresponding MCP servers.
Runtime Environment
Environment variables are read from process.env by default, but you can
override them when creating the Gram instance. This can be useful for testing
or local development. Example:
const g = new Gram({
envSchema: {
API_KEY: z.string().describe("API key for external service"),
BASE_URL: z.string().check(z.url()).describe("Base URL for API requests"),
},
env: {
API_KEY: "secret-key",
BASE_URL: "https://api.example.com",
},
});
If not provided, the framework falls back to process.env.
Response Types
The framework supports multiple response types. All response methods return Web API Response objects.
JSON Response
return ctx.json({
status: "success",
data: { id: 123, name: "Example" },
});
Text Response
return ctx.text("Plain text response");
HTML Response
return ctx.html(`
<!DOCTYPE html>
<html>
<body><h1>Hello</h1></body>
</html>
`);
Custom Response
You can also return a plain Response object:
return new Response(data, {
status: 200,
headers: {
"Content-Type": "application/xml",
"X-Custom-Header": "value",
},
});
Error Handling
Using ctx.fail()
Use ctx.fail() to throw error responses:
async execute(ctx, input) {
if (!input.userId) {
ctx.fail(
{ error: "userId is required" },
{ status: 400 }
);
}
const user = await fetchUser(input.userId);
if (!user) {
ctx.fail(
{ error: "User not found" },
{ status: 404 }
);
}
return ctx.json({ user });
}
Errors automatically include a stack trace in the response.
Using assert()
The assert function provides a convenient way to validate conditions and throw error responses:
import { assert } from "@gram-ai/functions";
async execute(ctx, input) {
assert(input.userId, { error: "userId is required" }, { status: 400 });
const user = await fetchUser(input.userId);
assert(user, { error: "User not found" }, { status: 404 });
return ctx.json({ user });
}
The assert function throws a Response object when the condition is false. The framework catches all thrown values, and if any happen to be a Response instance, they will be returned to the client.
Key points about assert:
- First parameter is the condition to check
- Second parameter is the error data (must include an
errorfield) - Third parameter is optional and can specify the status code (defaults to 500)
- Automatically includes a stack trace in the response
- Uses TypeScript's assertion type to narrow types when the assertion passes
Manifest Generation
Generate a manifest of all registered tools:
const g = new Gram().tool({/* ... */}).tool({/* ... */});
const manifest = g.manifest();
// {
// version: "0.0.0",
// tools: [
// {
// name: "tool1",
// description: "...",
// inputSchema: "...", // JSON Schema string
// variables: { ... }
// },
// ...
// ]
// }
Handling Tool Calls
Export the handleToolCall method to process incoming requests:
const g = new Gram().tool({/* ... */}).tool({/* ... */});
export const handleToolCall = g.handleToolCall;
You can also call tools programmatically:
const response = await g.handleToolCall({
name: "add",
input: { a: 5, b: 3 },
});
const data = await response.json();
console.log(data); // { sum: 8 }
With abort signal support:
const controller = new AbortController();
const responsePromise = g.handleToolCall(
{ name: "longRunning", input: {} },
{ signal: controller.signal },
);
// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);
Type Safety
The framework provides full TypeScript type inference:
const g = new Gram().tool({
name: "greet",
inputSchema: { name: z.string() },
async execute(ctx, input) {
// input.name is typed as string
return ctx.json({ message: `Hello, ${input.name}` });
},
});
// Type-safe tool calls
const response = await g.handleToolCall({
name: "greet", // Only "greet" is valid
input: { name: "World" }, // input is typed correctly
});
// Response type is inferred
const data = await response.json(); // { message: string }