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

@gram-ai/functions

v0.13.0

Published

Gram Functions are small pieces of code that represent LLM tools. They are deployed to [Gram](https://getgram.ai) and are then exposed to LLMs via MCP servers.

Readme

Gram Functions for TypeScript

Gram Functions are small pieces of code that represent LLM tools. They are deployed to Gram and are then exposed to LLMs via MCP servers.

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

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;

Quickstart

You can use one of the following command to scaffold a new Gram Function project quickly:

pnpm create @gram-ai/function@latest --template gram

## Or one of the following:
# bun create @gram-ai/function@latest --template gram
# npm create @gram-ai/function@latest -- --template gram

Installation

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

pnpm add @gram-ai/functions

## Or one of the following:
# bun add @gram-ai/functions
# npm add @gram-ai/functions

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 gram = 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 default gram;

Composing Gram Instances

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

import { Gram } from "@gram-ai/functions";
import * as z from "zod/mini";

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

// String tools
const stringTools = new Gram()
  .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 gram = mathTools.extend(stringTools);

export default gram;

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 Gram 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 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 { Gram } from "@gram-ai/functions";
import * as z from "zod/mini";

const gram = new Gram().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 gram = 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 { Gram } from "@gram-ai/functions";
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:

import { Gram } from "@gram-ai/functions";
import * as z from "zod/mini";

const gram = new Gram({
  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. Gram will handle the OAuth flow and inject the acquired token into the specified environment variable:

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

User Identity

When an authenticated Gram 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 GRAM_USER_EMAIL environment variable:

const gram = new Gram({
  envSchema: {
    OAUTH_TOKEN: z.optional(z.string()),
    GRAM_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.GRAM_USER_EMAIL;
    if (!email) {
      return ctx.json({ authenticated: false });
    }
    return ctx.json({ authenticated: true, email });
  },
});

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

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 error field)
  • 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:

import { Gram } from "@gram-ai/functions";

const gram = new Gram()
  .tool({
    /* ... */
  })
  .tool({
    /* ... */
  });

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

Handling Tool Calls

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

import { Gram } from "@gram-ai/functions";

const gram = new Gram()
  .tool({
    /* ... */
  })
  .tool({
    /* ... */
  });

export default gram;

You can also call tools programmatically:

const response = await gram.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 gram.handleToolCall(
  { name: "longRunning", input: {} },
  { signal },
);

Type Safety

The framework provides full TypeScript type inference:

import { Gram } from "@gram-ai/functions";
import * as z from "zod/mini";

const gram = 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 }