# @zapier/mcp-integration

> SDK for using remote Model Context Protocol (MCP) servers in/as Zapier integrations.

[![npm version](https://badge.fury.io/js/@zapier%2Fmcp-integration.svg)](https://www.npmjs.com/package/@zapier/mcp-integration)

## Table of Contents

- [Prerequisites](#prerequisites)
- [Create an MCP integration](#create-an-mcp-integration)
- [Update an MCP integration](#update-an-mcp-integration)
- [Customize generated actions](#customize-generated-actions)

## Prerequisites

To use `@zapier/mcp-integration`, a Zapier integration needs to:

- Use `zapier-platform-core@17.7.0` or later.
- Use TypeScript as language, and ESM modules.
- Have been opted in to use `z.cache()`.

The remote MCP server needs to:

- Support OAuth2 with Dynamic Client Registration.
- StreamableHTTP or SSE.

## Create an MCP integration

To create an integration that primarily uses MCP, including to handle authentication, follow these steps.

1. Initialize the integration:

   ```bash
   zapier init my-integration --language=typescript --module esm --template oauth2
   cd my-integration
   ```

   > **NOTE:** We will provide a `mcp` template to replace some of the following steps.

2. Remove files we we won't need:

   ```bash
   rm -rf src/test src/authentication.ts src/middleware.ts
   ```

3. Install this package and all other dependencies:

   ```bash
   npm install @zapier/mcp-integration
   ```

4. Create `src/mcp.ts`:

   ```ts
   import { MCPIntegration } from '@zapier/mcp-integration';

   import packageJson from '../package.json' with { type: 'json' };

   export default new MCPIntegration({
     name: packageJson.name,
     version: packageJson.version,
     serverUrl: 'https://example.com/mcp',
     transport: 'streamable', // or 'sse'
     auth: {
       type: 'oauth2',
     },
   });
   ```

   Replace `https://example.com/mcp` with your remote MCP server URL, and change `transport` when needed.

5. Create `src/actions.generated.ts`:

   ```ts
   export default {};
   ```

   This is just for the next step to compile before we have actually generated actions.

6. Replace `src/index.ts`:

   ```ts
   import zapier from 'zapier-platform-core';

   import packageJson from '../package.json' with { type: 'json' };

   import mcp from './mcp.js';
   import actions from './actions.generated.js';

   export default mcp.defineApp({
     version: packageJson.version,
     platformVersion: zapier.version,
     ...actions,
   });
   ```

7. Compile:

   ```bash
   npm run build
   ```

   > **NOTE:** To not have to repeat this, add `"dev": "npm run build -- --watch",` to your `scripts` in `package.json`, and keep a terminal window open running `npm run dev`.

8. Create `.env`:

   ```bash
   CLIENT_ID='dynamic'
   CLIENT_SECRET='dynamic'
   ```

   This is because `zapier invoke auth` does not support Dynamic Client Registration and assumes these to be needed.

9. Create a local connection:

   ```bash
   zapier invoke auth start
   ```

   > **NOTE:** If you run into issues, make sure you at on 17.7.0 or later.

10. Generate the actions:

    ```bash
    npx @zapier/mcp-integration generate
    ```

    > **NOTE:** If you get a "Missing or invalid access token" error, check `.env` to make sure there is a linebreak before `authData_type`. This is a `zapier-platform-cli` bug ([#1129](https://github.com/zapier/zapier-platform/pull/1129)).

11. Register the integration:

    ```bash
    zapier register
    ```

12. Push the integration:

    ```bash
    zapier push
    ```

13. Request to have your integration be opted in to to use `z.cache()`.

> **NOTE:** This feature isn't enabled by default yet. Staff can enable it using the `zache` switch.

## Update an MCP integration

You can run `npx @zapier/mcp-integration generate` at any time to generate actions for new tools and update actions when tools have changed.

> **NOTE:** We don't yet delete generated actions for tools that no longer exist, but they _will_ be dropped from `actions.generated.ts`.

After running the command again, run any linter and formatter tools before using `git diff` to inspect the changes.

## Customize generated actions

Once you have created or updated an MCP integration, run `zapier validate` and perform manual tests (e.g. in a Zap) to identify where actions need to be customized using a transformer.

### Setup

Create `src/transformer.ts`:

```ts
import { defineTransformer, extendAction } from '@zapier/mcp-integration';

export default defineTransformer({
  transformCreate: (action, tool) => {
    console.log(`Create: ${action.key} / Tool: ${tool.name}`);
    if (action.key === 'create_example') {
      return extendAction(action).build();
    } else {
      return action;
    }
  },
  transformSearch: (action, tool) => {
    console.log(`Search: ${action.key} / Tool: ${tool.name}`);
    return action;
  },
  transformTrigger: (action, tool) => {
    console.log(`Trigger: ${action.key} / Tool: ${tool.name}`);
    return action;
  },
});
```

Hook the transformer up via `src/mcp.ts`:

```ts
import transformer from './transformer.js';

export default new MCPIntegration({
  transformer,
  // ... other configuration options
});
```

The above setup won't have any effect. Read on how to chain calls on `extendAction` to customize them.

### Field Manipulation

Add fields:

```ts
extendAction(action)
  .prependFields({ key: 'token', type: 'string', required: true })
  .appendFields({ key: 'debug', type: 'boolean', default: false })
  .build();
```

Modify fields:

```ts
extendAction(action)
  .modifyField('page_id', (field) => ({
    ...field,
    dynamic: 'list_pages.id.name',
  }))
  .build();
```

Replace all fields:

```ts
extendAction(action)
  .replaceInputFields(
    { key: 'url', type: 'string', required: true },
    { key: 'method', type: 'string', choices: ['GET', 'POST', 'PUT'] },
  )
  .build();
```

### Input Field Groups

Organize fields in the UI with groups:

```ts
export const inputFieldGroups = [
  { key: 'content', label: 'Content', emphasize: true },
  { key: 'formatting', label: 'Formatting', emphasize: false },
];

export const inputFields = [
  { key: 'title', type: 'string', required: true, group: 'content' },
  { key: 'text', type: 'text', required: true, group: 'content' },
  {
    key: 'formatting',
    type: 'string',
    list: true,
    choices: { bold: 'Bold', italic: 'Italic' },
    group: 'formatting',
  },
];

extendAction(action)
  .replaceInputFields(withNamedImport('../utils/myAction.js', 'inputFields'))
  .replaceInputFieldGroups(
    withNamedImport('../utils/myAction.js', 'inputFieldGroups'),
  )
  .build();
```

### Dynamic Dropdowns

Link fields with given keys to use (custom) triggers to populate dynamic dropdowns:

```ts
const addDropdowns = addCommonDynamicDropdowns({
  page_id: 'list_pages.id.name',
  user_id: 'list_users.id.name',
});

extendAction(action).transformAllFields(addDropdowns).build();
```

### Drop actions

Return `undefined` instead of the (build customized) action to not have it generated.

### Control Action Types

```ts
transformTool: (tool) => {
  // Make read-only tools generate search/trigger instead of create
  if (tool.name === 'get-user') {
    return {
      ...tool,
      annotations: { ...tool.annotations, readOnlyHint: true },
    };
  }
  return undefined; // unchanged
},
```
