MCP Gateway / Using the Functions Framework
Using the Functions Framework
Learn how to use the Speakeasy Functions Framework with TypeScript, including context objects and best practices.
Overview
Section titled “Overview”The Speakeasy Functions Framework is a lightweight way to build MCP tools in TypeScript, published as the @speakeasy-api/functions package. It handles the MCP implementation so a function only has to contain its tool logic. A function bundle’s manifest and entrypoint can be written by hand, but the framework handles manifest generation, bundling, input validation, and packaging automatically at build time. Deployed functions appear on the Sources tab of MCP Gateway > MCP in the dashboard.
Previously @gram-ai/functions
The framework was published as @gram-ai/functions, with a Gram class and
withGram and fromGram helpers. Those names still work as aliases of
@speakeasy-api/functions, Functions, withFunctions and fromFunctions,
so existing projects do not need to change. Files named gram.config.ts,
src/gram.ts and gram.deploy.json are also still read.
The project scaffolder, speakeasy functions init, offers the framework as its first choice:

To skip the prompt and scaffold straight onto the framework, pass the functions template:
speakeasy functions init my-functions --template functionsFunction structure
Section titled “Function structure”Every Function follows this basic structure:
import { Functions } from "@speakeasy-api/functions";import * as z from "zod/mini";
const functions = new Functions().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 functions;Tool definition
Section titled “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 parametersannotations(optional): Behavior hints for AI models and clients (see Tool annotations)execute: An async function that implements the tool logic
A tool that computes a result from validated input:
import { Functions } from "@speakeasy-api/functions";import * as z from "zod/mini";
const functions = new Functions().tool({ name: "calculate_discount", description: "Calculate the final price after applying a discount percentage", inputSchema: { originalPrice: z.number().positive(), discountPercent: z.number().min(0).max(100), }, async execute(ctx, input) { const discount = input.originalPrice * (input.discountPercent / 100); const finalPrice = input.originalPrice - discount;
return ctx.json({ originalPrice: input.originalPrice, discount: discount, finalPrice: finalPrice, }); },});
export default functions;Context object
Section titled “Context object”The execute function receives a context object with several helper methods for handling responses and accessing configuration:
Response methods
Section titled “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 functions = new Functions().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
Section titled “Additional context properties”ctx.signal: AnAbortSignalfor handling cancellationctx.env: Access to parsed environment variables
const functions = new Functions().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; } },});Caller identity
Section titled “Caller identity”Tool calls that arrive over MCP carry attribution about the caller:
ctx.clientInfo: The name and version of the calling MCP client, when the client reports themctx.oauthClientId: The OAuth client ID attributed to the caller, when one accompanied the callctx.meta: The raw_metablock sent with the call, for keys the framework does not model yet
Every field is optional. Calls that do not arrive over MCP, such as direct instance invocations and resource reads, leave all three undefined.
const functions = new Functions().tool({ name: "record_action", inputSchema: { action: z.string() }, async execute(ctx, input) { console.log("tool call", { action: input.action, client: ctx.clientInfo?.name, clientVersion: ctx.clientInfo?.version, oauthClient: ctx.oauthClientId, }); return ctx.json({ recorded: true }); },});Attribution only
Caller identity is metadata attached to the request, and a tool cannot distinguish a value the host attached from one the caller supplied. Use these fields for logging, metrics, and telling one integration’s traffic from another’s. Never gate access on them. Authorization belongs to whatever verifies credentials in front of the tool, such as OAuth.
Input validation
Section titled “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
Section titled “Lax mode”To allow unvalidated inputs, enable lax mode:
const functions = new Functions({ 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
Section titled “Environment variables”Functions can access environment variables directly from process.env:
const functions = new Functions().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 Functions, see Configuring environments.
Using the Fetch API
Section titled “Using the Fetch API”Tools can make requests to downstream APIs and respond with the following result:
const functions = new Functions().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
Section titled “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 functions = new Functions().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
Section titled “Composability”Instances of Functions 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
Section titled “Basic composition”Split tool definitions into separate modules and combine them:
import { Functions } from "@speakeasy-api/functions";import * as z from "zod/mini";
export const trainFunctions = new Functions({ 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 { Functions } from "@speakeasy-api/functions";import * as z from "zod/mini";
export const flightFunctions = new Functions({ 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 { Functions } from "@speakeasy-api/functions";import { trainFunctions } from "./train";import { flightFunctions } from "./flight";
const functions = new Functions().extend(trainFunctions).extend(flightFunctions);
export default functions;Environment schema merging
Section titled “Environment schema merging”When composing Functions 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 requirementsconst weatherFunctions = new Functions({ 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 newsFunctions = new Functions({ 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 variablesconst functions = new Functions().extend(weatherFunctions).extend(newsFunctions);// Both WEATHER_API_KEY and NEWS_API_KEY must be providedBenefits of composition
Section titled “Benefits of composition”Composing Functions instances provides several advantages:
- Modularity: Organize related tools into separate files
- Reusability: Share tool definitions across different
Functionsinstances - Maintainability: Easier to manage large codebases with many tools
- Team collaboration: Different team members can work on separate modules
Next steps
Section titled “Next steps”- Build and deploy Functions.