npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@speakeasy-api/functions

v0.20.0

Published

Build MCP tools in TypeScript and deploy them with the Speakeasy CLI.

Readme

Speakeasy Functions for TypeScript

Speakeasy Functions are small pieces of code that represent LLM tools. They are deployed to the Speakeasy AI Control Plane and are then exposed to LLMs via MCP servers.

This library provides a small framework for authoring Speakeasy Functions in TypeScript. The "Hello, World!" example is:

import { Functions } from "@speakeasy-api/functions";
import * as z from "zod/mini";

const functions = new Functions().tool({
  name: "greet",
  description: "Greet someone special",
  inputSchema: { name: z.string() },
  async execute(ctx, input) {
    return ctx.json({ message: `Hello, ${input.name}!` });
  },
});

export default functions;

Quickstart

Install the Speakeasy AI Control Plane CLI with brew install speakeasy-api/tap/cli or npm i -g @speakeasy-api/cli, then scaffold, build and deploy a project:

speakeasy functions init my-tools
cd my-tools
speakeasy functions build
speakeasy functions push

speakeasy functions dev runs the project's dev script, which starts a local MCP server with the MCP Inspector. speakeasy functions build runs this package from the project's node_modules, so the build always matches the SDK version the project depends on.

npm create @gram-ai/function and the gf build and gf push commands still work but are deprecated in favour of the speakeasy functions commands.

Installation

Use one of the following commands to add the package to your project:

pnpm add @speakeasy-api/functions

## Or one of the following:
# bun add @speakeasy-api/functions
# npm add @speakeasy-api/functions

Migrating from @gram-ai/functions

This package was published as @gram-ai/functions. The old names are deprecated aliases and keep working, so you can migrate when it suits you:

| Old name | New name | | --------------------------------------- | ------------------------------------------- | | @gram-ai/functions (and its subpaths) | @speakeasy-api/functions | | Gram | Functions | | fromGram (/mcp) | fromFunctions | | withGram (/mcp) | withFunctions | | gram.config.{ts,mts,js,mjs} | speakeasy.config.{ts,mts,js,mjs} | | src/gram.ts (default entrypoint) | src/functions.ts | | gram.deploy.json | speakeasy.deploy.json | | dist/gram.zip | dist/functions.zip | | GRAM_USER_EMAIL | SPEAKEASY_AI_USER_EMAIL (both are set) | | GRAM_CLI_PATH, GRAM_DEV | SPEAKEASY_AI_CLI_PATH, SPEAKEASY_AI_DEV | | GRAM_* CLI environment variables | SPEAKEASY_AI_* |

@gram-ai/functions now re-exports this package at the same version, so import { Gram } from "@gram-ai/functions" and the gf command work as before. When a project has both a new and an old file name, the new one wins. The build prints a one-line note when it uses gram.config.* or gram.deploy.json.

Core Concepts

The Functions Instance

The Functions class is the main entry point for defining tools. You create an instance and chain .tool() calls to register multiple tools:

import { Functions } from "@speakeasy-api/functions";
import * as z from "zod/mini";

const functions = new Functions()
  .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 default functions;

Composing Functions Instances

You can compose multiple Functions instances together using the extend() method, similar to Hono's route groups pattern. This is useful for organizing tools by domain or functionality:

import { Functions } from "@speakeasy-api/functions";
import * as z from "zod/mini";

// Math tools
const mathTools = new Functions()
  .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 });
    },
  });

// String tools
const stringTools = new Functions().tool({
  name: "uppercase",
  description: "Convert string to uppercase",
  inputSchema: { text: z.string() },
  async execute(ctx, input) {
    return ctx.text(input.text.toUpperCase());
  },
});

// Combine both
const functions = mathTools.extend(stringTools);

export default functions;

The extend() method:

  • Merges tools: All tools from both instances are combined
  • Override behavior: If tool names collide, the extended instance's tools override the original's
  • Preserves context: Each tool maintains its original Functions instance's execution context (environment variables and lax validation settings)
  • Mutates original: Modifies and returns the original instance (not a copy)

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.markdown(data)

Returns a markdown response:

async execute(ctx, input) {
  return ctx.markdown("# Heading");
}

ctx.html(data)

Returns an HTML response:

async execute(ctx, input) {
  return ctx.html("<h1>Hello, World!</h1>");
}

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 Functions instance:

const functions = new Functions({
  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 { Functions } from "@speakeasy-api/functions";
import * as z from "zod/mini";

const functions = new Functions().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 functions = new Functions({ lax: true });

Environment Variables

Defining Variables

Environment variables that are used by tools must be defined when instantiating the Functions class. This is done using a Zod v4 object schema:

import { Functions } from "@speakeasy-api/functions";
import * as z from "zod/mini";

const functions = new Functions({
  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 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 Functions instance. This can be useful for testing or local development. Example:

import { Functions } from "@speakeasy-api/functions";
import * as z from "zod/mini";

const functions = new Functions({
  env: {
    API_KEY: "secret-key",
    BASE_URL: "https://api.example.com",
  },
  envSchema: {
    API_KEY: z.string().describe("API key for external service"),
    BASE_URL: z.string().check(z.url()).describe("Base URL for API requests"),
  },
});

If not provided, the framework falls back to process.env.

Authentication & Identity

OAuth Tokens

If your function needs to access external APIs on behalf of the user, you can declare an OAuth variable in authInput. Speakeasy will handle the OAuth flow and inject the acquired token into the specified environment variable:

const functions = new Functions({
  envSchema: {
    OAUTH_TOKEN: z.optional(z.string()),
  },
  authInput: {
    oauthVariable: "OAUTH_TOKEN",
  },
});

User Identity

When an authenticated user invokes a tool, you can opt in to receiving their email address by setting gramEmail: true in authInput. The email will be available as the SPEAKEASY_AI_USER_EMAIL environment variable. The deprecated GRAM_USER_EMAIL variable carries the same value, so existing code keeps working:

const functions = new Functions({
  envSchema: {
    OAUTH_TOKEN: z.optional(z.string()),
    SPEAKEASY_AI_USER_EMAIL: z.optional(z.string()),
  },
  authInput: {
    oauthVariable: "OAUTH_TOKEN",
    gramEmail: true,
  },
}).tool({
  name: "whoami",
  description: "Returns the current user's email",
  inputSchema: {},
  async execute(ctx) {
    const email = ctx.env.SPEAKEASY_AI_USER_EMAIL;
    if (!email) {
      return ctx.json({ authenticated: false });
    }
    return ctx.json({ authenticated: true, email });
  },
});

SPEAKEASY_AI_USER_EMAIL will be empty when the request is unauthenticated.

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 });
}

The response body is exactly the data you pass — no stack trace is added, since a deployed function is a minified bundle whose frames mean nothing to the tool's caller and cost an MCP client context on every turn that follows.

A tool call that fails input validation is reported the same way: error names each offending input and what was wrong with it on one line, alongside the structured issues from Zod.

{
  "error": "org_id: Invalid input: expected string, received undefined",
  "issues": [
    {
      "expected": "string",
      "code": "invalid_type",
      "path": ["org_id"],
      "message": "Invalid input: expected string, received undefined"
    }
  ]
}

Using assert()

The assert function provides a convenient way to validate conditions and throw error responses:

import { assert } from "@speakeasy-api/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 error field)
  • Third parameter is optional and can specify the status code (defaults to 500)
  • Serializes only the data you pass; no stack trace is added
  • Uses TypeScript's assertion type to narrow types when the assertion passes

Manifest Generation

Generate a manifest of all registered tools:

import { Functions } from "@speakeasy-api/functions";

const functions = new Functions().tool({/* ... */}).tool({/* ... */});

const manifest = functions.manifest();
// {
//   version: "0.0.0",
//   tools: [
//     {
//       name: "tool1",
//       description: "...",
//       inputSchema: "...", // JSON Schema string
//       variables: { ... }
//     },
//     ...
//   ]
// }

Deploying

Deploy with speakeasy functions push. It builds the project, adds the zip file to the deployment file (speakeasy.deploy.json by default, or gram.deploy.json when only that file exists) and pushes a deployment. The target project is --project (or SPEAKEASY_AI_PROJECT), then deployProject in speakeasy.config.ts, then the project you chose with speakeasy auth. The slug comes from --slug, then slug in speakeasy.config.ts, then the package.json name without its scope. Pass --no-build to deploy the existing build output.

Project config

The build reads the first speakeasy.config.{ts,mts,js,mjs} file in the project directory:

import { defineConfig } from "@speakeasy-api/functions/build";

export default defineConfig({
  entrypoint: "src/functions.ts",
  slug: "my-tools",
});

Without an entrypoint, the build uses src/functions.ts, or src/gram.ts when only that file exists. The build writes dist/functions.zip.

Building programmatically

@speakeasy-api/functions/build exports the build that speakeasy functions build runs:

import { build, resolveProject } from "@speakeasy-api/functions/build";

// Builds dist/functions.zip and returns the resolved project settings.
const { project, files } = await build({ cwd: "./my-tools" });

// Resolves the same settings without building.
const settings = await resolveProject({ cwd: "./my-tools", outDir: "out" });

Both accept cwd, configFile, entrypoint and outDir; the last three override speakeasy.config.ts.

Deploying with gf

The deprecated gf build and gf push commands ship with @gram-ai/functions. gf push runs the Speakeasy AI Control Plane CLI. It picks the CLI in this order:

  1. The path in the SPEAKEASY_AI_CLI_PATH (or deprecated GRAM_CLI_PATH) environment variable.
  2. cli/bin/gram from this repository, when SPEAKEASY_AI_DEV (or deprecated GRAM_DEV) is 1 or true.
  3. speakeasy on your PATH, if speakeasy --control-plane-cli confirms it is the AI Control Plane CLI. The Speakeasy SDK generator CLI also installs a speakeasy binary, and the SDK skips it.
  4. The legacy gram command.

Handling Tool Calls

Exporting the Functions instance from your module as the default export will allow Speakeasy to handle tool calls automatically when deployed:

import { Functions } from "@speakeasy-api/functions";

const functions = new Functions().tool({/* ... */}).tool({/* ... */});

export default functions;

You can also call tools programmatically:

const response = await functions.handleToolCall({
  name: "add",
  input: { a: 5, b: 3 },
});

const data = await response.json();
console.log(data); // { sum: 8 }

With abort signal support:

const signal = AbortSignal.timeout(5000);

const response = await functions.handleToolCall(
  { name: "longRunning", input: {} },
  { signal },
);

Type Safety

The framework provides full TypeScript type inference:

import { Functions } from "@speakeasy-api/functions";
import * as z from "zod/mini";

const functions = new Functions().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 functions.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 }