Using the Functions Framework
Overview
The Gram Functions Framework provides a streamlined way to build MCP tools using TypeScript. It handles MCP implementation while letting you focus on your tool logic.

Function structure
Every Gram Function follows this basic structure:
import { Gram } from "@gram-ai/functions";
import * as z from "zod/mini";
const gram = new Gram().tool({
name: "add",
description: "Add two numbers together",
inputSchema: { a: z.number(), b: z.number() },
async execute(ctx, input) {
return ctx.json({sum: input.a + input.b});
},
});
export default gram;Tool definition
Each tool requires the following properties:
name: A unique identifier for the tooldescription(optional): A human-readable explanation of what the tool doesinputSchema: A Zod schema defining the expected input parametersexecute: An async function that implements the tool logic
Context object
The execute function receives a context object with several helper methods for handling responses and accessing configuration:
Response methods
ctx.json(data): Returns a JSON responsectx.text(data): Returns a plain text responsectx.html(data): Returns an HTML responsectx.fail(data, options?): Throws an error response
const gram = new Gram().tool({
name: "format_data",
inputSchema: { format: z.enum(["json", "text", "html"]), data: z.string() },
async execute(ctx, input) {
if (input.format === "json") {
return ctx.json({ data: input.data });
} else if (input.format === "text") {
return ctx.text(input.data);
} else {
return ctx.html(`<div>${input.data}</div>`);
}
},
});Additional context properties
ctx.signal: AnAbortSignalfor handling cancellationctx.env: Access to parsed environment variables
const gram = new Gram().tool({
name: "long_running_task",
inputSchema: { url: z.string() },
async execute(ctx, input) {
try {
const response = await fetch(input.url, { signal: ctx.signal });
return ctx.json(await response.json());
} catch (error) {
if (error.name === "AbortError") {
return ctx.fail("Request was cancelled");
}
throw error;
}
},
});Input validation
The Functions Framework validates inputs against the provided Zod schema by default. For strict validation, it rejects inputs that don’t match the schema.
Lax mode
To allow unvalidated inputs, enable lax mode:
const gram = new Gram({ lax: true }).tool({
name: "flexible_tool",
inputSchema: { required: z.string() },
async execute(ctx, input) {
// input may contain additional properties not in the schema
return ctx.json({ received: input });
},
});Environment variables
Gram Functions can access environment variables directly from process.env:
const gram = new Gram().tool({
name: "api_call",
inputSchema: { endpoint: z.string() },
async execute(ctx, input) {
const apiUrl = process.env.API_URL;
const response = await fetch(`${apiUrl}/${input.endpoint}`);
return ctx.json(await response.json());
},
});For more details on configuring and managing environment variables in Gram Functions, see Configuring environments.
Using the Fetch API
Tools can make requests to downstream APIs and respond with the following result:
const gram = new Gram().tool({
name: "spacex-ships",
description: "Get the latest SpaceX ship list",
inputSchema: {},
async execute(ctx) {
const response = await fetch("https://api.spacexdata.com/v3/ships");
return ctx.json(await response.json());
},
});Response flexibility
Tools can return responses in multiple formats:
- JSON responses via
ctx.json() - Plain text via
ctx.text() - HTML content via
ctx.html() - Custom Web API response objects with specific headers and status codes
const gram = new Gram().tool({
name: "custom_response",
inputSchema: { code: z.number() },
async execute(ctx, input) {
return new Response("Custom response", {
status: input.code,
headers: { "X-Custom-Header": "value" },
});
},
});Composability
Gram instances can be composed together using the .extend() method, allowing tool definitions to be split across multiple files and modules. This pattern is similar to Hono’s grouping pattern and helps organize larger codebases.
Basic composition
Split tool definitions into separate modules and combine them:
import { Gram } from "@gram-ai/functions";
import * as z from "zod/mini";
export const trainGram = new Gram({
envSchema: {
TRAIN_API_KEY: z.string().describe("API key for the train service"),
},
})
.tool({
name: "train_book",
description: "Books a train ticket",
inputSchema: { destination: z.string(), date: z.string() },
async execute(ctx, input) {
const apiKey = ctx.env.TRAIN_API_KEY;
// Implementation here
return ctx.json({ booked: true });
},
})
.tool({
name: "train_status",
description: "Gets the status of a train",
inputSchema: { trainId: z.string() },
async execute(ctx, input) {
// Implementation here
return ctx.json({ status: "on time" });
},
});import { Gram } from "@gram-ai/functions";
import * as z from "zod/mini";
export const flightGram = new Gram({
envSchema: {
FLIGHT_API_KEY: z.string().describe("API key for the flight service"),
},
})
.tool({
name: "flight_book",
description: "Books a flight ticket",
inputSchema: { destination: z.string(), date: z.string() },
async execute(ctx, input) {
const apiKey = ctx.env.FLIGHT_API_KEY;
// Implementation here
return ctx.json({ booked: true });
},
})
.tool({
name: "flight_status",
description: "Gets the status of a flight",
inputSchema: { flightNumber: z.string() },
async execute(ctx, input) {
// Implementation here
return ctx.json({ status: "departed" });
},
});import { Gram } from "@gram-ai/functions";
import { trainGram } from "./train";
import { flightGram } from "./flight";
const gram = new Gram()
.extend(trainGram)
.extend(flightGram);
export default gram;Environment schema merging
When composing Gram instances, environment schemas are automatically merged. Each module can define its own environment variables, and the final composed instance will validate all required variables:
// Each module defines its own environment requirements
const weatherGram = new Gram({
envSchema: {
WEATHER_API_KEY: z.string(),
},
}).tool({
name: "get_weather",
inputSchema: { city: z.string() },
async execute(ctx, input) {
// Access environment variable from this module
const apiKey = ctx.env.WEATHER_API_KEY;
return ctx.json({ temperature: 72 });
},
});
const newsGram = new Gram({
envSchema: {
NEWS_API_KEY: z.string(),
},
}).tool({
name: "get_news",
inputSchema: { topic: z.string() },
async execute(ctx, input) {
// Access environment variable from this module
const apiKey = ctx.env.NEWS_API_KEY;
return ctx.json({ articles: [] });
},
});
// Composed instance requires both environment variables
const gram = new Gram()
.extend(weatherGram)
.extend(newsGram);
// Both WEATHER_API_KEY and NEWS_API_KEY must be providedBenefits of composition
Composing Gram instances provides several advantages:
- Modularity: Organize related tools into separate files
- Reusability: Share tool definitions across different Gram instances
- Maintainability: Easier to manage large codebases with many tools
- Team collaboration: Different team members can work on separate modules
Next steps
- Build and deploy Gram Functions.
Last updated on