# 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.

![Choosing the Gram Framework](/assets/docs/gram/img/functions/npm_gram-framework.png)

## Function structure

Every Gram Function follows this basic structure:

```typescript filename="gram.ts"

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

```

## Tool definition

Each tool requires the following properties:

- **`name`:** A unique identifier for the tool
- **`description`(optional):** A human-readable explanation of what the tool does
- **`inputSchema`:** A Zod schema defining the expected input parameters
- **`annotations`(optional):** Behavior hints for AI models and clients (see [Tool annotations](/docs/mcp/build/gram-functions/tool-annotations))
- **`execute`:** 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 response
- **`ctx.text(data)`:** Returns a plain text response
- **`ctx.html(data)`:** Returns an HTML response
- **`ctx.fail(data, options?)`:** Throws an error response

```typescript
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`:** An `AbortSignal` for handling cancellation
- **`ctx.env`:** Access to parsed environment variables

```typescript
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:

```typescript
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`:

```typescript
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](/docs/mcp/build/gram-functions/configuring-environments).

## Using the Fetch API

Tools can make requests to downstream APIs and respond with the following result:

```typescript
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

```typescript
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:

```typescript filename="train.ts"

      // 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" });
    },
  });
```

```typescript filename="flight.ts"

      // 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" });
    },
  });
```

```typescript filename="gram.ts"

const gram = new Gram()
  .extend(trainGram)
  .extend(flightGram);

```

### 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:

```typescript
// 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 provided
```

### Benefits 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](/docs/mcp/build/gram-functions/build-deploy) Gram Functions.
