Skip to content
Status

MCP Gateway / Add tools with TypeScript

Add tools with TypeScript

Write custom tools with the TypeScript functions framework, test them locally, and deploy them as a source.

Speakeasy Functions are compact code units that represent LLM tools. Write them in TypeScript, deploy them to the platform, and expose them to language models through MCP servers. Once deployed, Functions behave like any other tool on the platform, so they can be hosted as MCP servers, combined with tools from other sources on a single server, or used to build multi-tool workflows. Deployed functions appear on the Sources tab of MCP Gateway > MCP in the dashboard.

Unlike OpenAPI-sourced tools, which wrap existing API endpoints, functions run custom code in isolated environments. A function can call multiple APIs, connect to remote databases over TCP or HTTP, and transform data with third-party libraries. Function-sourced tools suit:

  • Custom business logic: Calculations, data processing, or decision-making that doesn’t map to a single API endpoint, such as aggregating data from multiple sources
  • Data transformations: Filtering, enriching, or summarizing data before returning it to the LLM
  • Multi-step workflows: Orchestrating several API calls or operations in a single tool
  • Services without an OpenAPI document: Integrating third-party services or databases that no OpenAPI document describes
  • Control flow: Conditional logic, iteration, and other control flow that a declarative API specification can’t express
import { Gram } from "@gram-ai/functions";
import * as z from "zod/mini";
const gram = new Gram().tool({
name: "greet",
description: "Greet someone special",
inputSchema: { name: z.string() },
async execute(ctx, input) {
return ctx.json({ message: `Hello, ${input.name}!` });
},
});
export default gram;

The steps below also appear in the dashboard: go to MCP Gateway > MCP, click Add new, and choose Write custom code under Advanced.

Pushing functions with the gram CLI and opening the Write custom code page require the project:write scope. Building an MCP server from the deployed functions requires the mcp:write scope. The default Admin role includes both. The Write custom code option is shown only when functions are enabled for the organization; the CLI commands below work regardless.

This guide assumes:

  • An account and project exist (see Getting started)
  • Node.js version 22.18.0 or later is installed

Scaffold a new project:

Terminal window
npm create @gram-ai/function@latest

The scaffolder prompts for:

  • Framework: Gram Functions (the simplest path, batteries included) or the official Model Context Protocol SDK for advanced use cases
  • Project name, directory, and git initialization
  • Dependency installation
  • CLI installation: The gram CLI is required to deploy. The scaffolder offers to install it and run gram auth to authenticate

To skip the framework prompt and scaffold directly onto the Functions Framework, pass a template (gram or mcp):

Terminal window
npm create @gram-ai/function@latest -- --template gram

The project contains src/gram.ts (the tools) and src/server.ts (a local MCP server for testing):

Terminal window
└── src
├── gram.ts # Edit me!
└── server.ts
└── package.json
└── README.md

Open src/gram.ts and define tools, as many as needed:

import { Gram } from "@gram-ai/functions";
import * as z from "zod/mini";
const gram = new Gram().tool({
name: "greet",
description: "Greet someone special",
inputSchema: { name: z.string() },
async execute(ctx, input) {
return ctx.json({ message: `Hello, ${input.name}!` });
},
});
export default gram;

Tools can be written with the Functions Framework or the official MCP SDK. The Functions Framework is the more lightweight way to write tools, and MCP SDK support is available for teams that prefer the official SDK.

Tools, not servers

These are tools, not a finished MCP server. After deploying, tools can be sliced into multiple MCP servers or combined with tools from other sources.

Test locally before deploying. Running npm run dev starts the project under the MCP Inspector, so tools can be called interactively from the browser.

Terminal window
npm run build
npm run push

Build bundles the functions. Push stages the bundle and creates a deployment, running the gram CLI commands gram stage function and gram push under the hood. The CLI prints a link to the deployment page. Once the deployment completes, the functions source and its tools appear on the Sources tab of MCP Gateway > MCP in the dashboard. Deployment history and retries live on the Deployments tab, and the Build and deploy guide covers the process in full.

Open the deployed function on the Sources tab and click Build a server, or go to MCP Gateway > MCP, click Add new, and choose From an existing source under Advanced. Pick the function, enter a Server name, and click Create. Every tool the function produced is added to the new server, and the dashboard opens the server’s Tools tab, where tools from other sources can be added alongside them. The server’s detail page links to a hosted installation page with per-client setup instructions. See Authentication for visibility, Team access for who can connect, and Distribution for publishing.

Functions provide a way to create individual tools for use in MCP servers. Creating tools is not the same as creating MCP servers.

The key distinction:

  • Functions: Individual tools that represent specific capabilities or operations
  • MCP servers: Collections of tools combined into a single server that LLM clients can access

Once a tool exists, whether it came from Functions or OpenAPI, combine it with other tools into one or many MCP servers. Functions can therefore be split across multiple projects, kept in a single file, or organized however a workflow requires.

MCP servers are assembled from these tools later in the dashboard.

The scaffolded project produces a function bundle when it builds, and the Functions Framework generates everything in it. A bundle can also be written by hand. At its core, a function bundle is a zip file containing two files.

The manifest.json file describes the tools:

{
"version": "0.0.0",
"tools": [
{
"name": "add",
"description": "Add two numbers",
"inputSchema": {
"type": "object",
"properties": {
"a": { "type": "number" },
"b": { "type": "number" }
},
"required": ["a", "b"]
}
},
{
"name": "square_root",
"description": "Calculate the square root of a number",
"inputSchema": {
"type": "object",
"properties": {
"a": { "type": "number" }
},
"required": ["a"]
}
}
]
}

The manifest includes each tool’s name, description, and JSON Schema for input validation. It can also declare required environment variables, tags, and MCP tool annotations such as the read-only and destructive hints.

The functions.js file is bundled JavaScript that exports a handleToolCall function:

export async function handleToolCall({ name, input }) {
switch (name) {
case "add":
return json({ value: input.a + input.b });
case "square_root":
return json({ value: Math.sqrt(input.a) });
default:
throw new Error(`Unknown tool: ${name}`);
}
}

When the zip file is uploaded, the platform uses the manifest to expose the tools and invokes handleToolCall to execute them. Functions run on a managed runtime. The supported runtimes are Node.js 22 (nodejs:22), Node.js 24 (nodejs:24), and Python 3.12 (python:3.12).

Note

The code entrypoint must be named functions with an extension matching the runtime: functions.js, functions.mjs, functions.ts, or functions.mts for Node.js, and functions.py for Python.

Bundle size limit

The zipped bundle (containing both the manifest and the code entrypoint) must not exceed 15MB. Keep functions lean by avoiding large dependencies and using tree-shaking when bundling.