# Connect an MCP client

SambungAPI serves the tools of one [project](/glossary#glossary-term-project) to an MCP client, such as Claude Code or Cursor, at one URL. This page says what that URL takes, which tools the client sees, how an agent connects an application user's account, and what a call costs.

## The endpoint

The MCP endpoint is `https://sabi.metatech.id/mcp`. It speaks the Streamable HTTP transport: each `POST` carries one JSON-RPC message and gets one `application/json` answer. `GET` and `DELETE` answer `405`, because there is no event stream and no session.

| Protocol revision | How the endpoint serves it |
| --- | --- |
| `2026-07-28` | With no session: the client calls `server/discover`, and every request carries its own protocol version. |
| `2025-11-25`, `2025-06-18`, `2025-03-26`, `2024-11-05` | With `initialize`, but with no session: the endpoint never sends `Mcp-Session-Id`, and a request after `initialize` needs none. |

`server/discover` and `initialize` advertise one capability, `tools`, and the server name `sambungapi`. They also return `instructions`: a short text for the model about what the tools do, whose account a call uses, and what to do when an account is missing.

## The credential

A project key is the credential your software uses to run tools for one project. It is the same `ak_` key that runs `POST /v1/tools/{slug}/execute`, and you mint it in the console at `/app/projects/{project_id}/keys`. An agent with no key asks a person for one with a key request: see the key request in the [API guide](/docs/api). An agent with no project key asks a person for one with a Key request, or uses machine sign-in to get a Control-plane token and mint a project key: see [Get a project key](/docs/api). A Control-plane token alone cannot call /mcp. The [API guide](/docs/api) says who can mint one.

Send the [project key](/glossary#glossary-term-api-key) as `Authorization: Bearer ak_…`, or in the `x-api-key` header. The endpoint never reads a credential from the URL, so never put the key there: a URL is kept in logs and in client settings that other people read.

The endpoint accepts a project key and nothing else. A console session cookie or a personal [control-plane](/glossary#glossary-term-control-plane) token (`cpt_`) answers `401 Auth_MissingProjectKey`. A request from a web page whose `Origin` the deployment does not allow answers `403 Mcp_OriginForbidden`; an MCP client sends no `Origin`.

## The application user

Each tool call acts for one application user: the end user of your software whose provider account the tool uses. Name the application user in the URL, as the `user_id` query parameter, or in the `x-user-id` header:

```
https://sabi.metatech.id/mcp?user_id=user-123
```

The same value goes in the `user_id` of a connect link and of the execute body. A `user_id` in the query and an `x-user-id` header with a different value answer `400 BadRequest`, and so does a value that starts with `org:` and is not `org:<organization_id>` of the [organization](/glossary#glossary-term-organization) that owns the project of the key. A tool call of a [toolkit](/glossary#glossary-term-toolkit) whose connection scope is `user`, with no application user in the URL, is a tool error that names `user_id`, and nothing runs.

To act for the accounts that the project shares, name `org:<organization_id>` as the user id in the URL. Every toolkit connects one account for each application user by default. `GET /v1/tools/toolkits` gives each toolkit's `connection_scope`. A tool of a toolkit with `connection_scope: organization`, which only the deployment's operator can set, runs on the project's shared account under `org:<organization_id>`, whatever application user the URL names. A shared account serves only the project that connected it.

## Which tools the list holds

`tools/list` holds the tools of the toolkits that this deployment offers, and the tools of the toolkits that the organization of the key has an auth config for. A toolkit is offered when `GET /v1/tools/toolkits` gives it `available: true`. The list holds no deprecated tool and no tool of a channel, such as Slack. Each tool has its slug as `name`, its name as `title`, its description, and its input schema as `inputSchema`. The list is in catalog order, and a client can keep it for five minutes (`ttlMs: 300000`). The tool catalog at [`/v1/specs/catalog`](/v1/specs/catalog), which needs no credential, holds more: it has no organization, so it lists the tools of every toolkit that is not a channel, also when the deployment does not offer the toolkit and your organization has no auth config for it. The page [The tool catalog](/docs/tools) shows that catalog to a person: the arguments of each tool of an offered toolkit, and only the slugs of the tools of a toolkit that the deployment does not offer.

Two query parameters narrow the list. A narrowed tool is also one that a call cannot reach.

| Parameter | Value | What it does |
| --- | --- | --- |
| `toolkits` | Toolkit slugs, comma-separated, for example `gmail,googlecalendar` | Lists only the tools of these toolkits. |
| `tools` | Tool slugs, comma-separated, for example `GMAIL_SEND_EMAIL` | Lists only these tools. |

A value that names no toolkit or tool of the catalog answers `400 BadRequest`. `GET /v1/tools/toolkits` and `GET /v1/tools` list the catalog.

The arguments of a call are checked against the tool's input schema before anything runs. Arguments that do not match are a tool error, `BadRequest` with the message "Error in arguments.<field>: …", and no provider is called.

## Connect an application user's account

A tool runs on the application user's `ACTIVE` [connected account](/glossary#glossary-term-connected-account) for its toolkit. When the application user has none, the call is a tool error, `ActionExecute_ConnectedAccountNotFound`, whose `suggested_fix` says what to do next. An agent cannot open the console, so the endpoint gives it two ways to send the application user to a connect link.

**The tool `create_connect_link`.** The list holds it after the catalog tools, when it holds a tool of an offered toolkit that can get a connect link for the user id of the URL: a toolkit whose connection scope is `user`, or, when the URL names `org:<organization_id>`, also a toolkit whose connection scope is `organization`. Its `toolkit` values also hold `googlesuper` when the list holds a tool of a Google Workspace service: one consent connects them all. Its arguments are `toolkit`, one of the toolkits its input schema names, and an optional `auth_config_id`. It answers `redirect_url`, `expires_at` and `connected_account_id`. Give `redirect_url` to the application user: the link expires in 10 minutes. After the application user authorizes the toolkit, call the tool again.

- With no `auth_config_id`, the link uses the toolkit's only custom auth config, else its oldest managed one. With none, it creates a managed one when the deployment holds an OAuth application for the toolkit, and answers `Toolkit_NotConnectable` when it does not. A toolkit with several custom auth configs answers `AuthConfig_Ambiguous`, whose message lists their ids; call again with one of them.
- When the application user already has an account of that toolkit that waits for authorization, the link is a new one for that account.
- A toolkit that the deployment's operator made shared in each project answers `ScopePolicy_OrgShared`, unless the URL names `org:<organization_id>`. A toolkit this deployment cannot connect answers `Toolkit_NotConnectable`.

**The browser prompt.** A client that speaks revision `2026-07-28` and declares URL-mode elicitation does not get the tool error. The call asks the client to open the connect link instead, and Claude Code asks whether to open it in your browser. After the application user authorizes the toolkit, the client sends the call again, and the tool runs on the new account. When the consent is not finished, or the user declined to open the link, the answer is `ActionExecute_ConsentPending`; finish the consent and call again. The endpoint never waits for a consent. A client of an earlier revision, and a client that does not declare URL mode, gets the tool error and uses `create_connect_link`.

**The tools `list_connected_accounts` and `delete_connected_account`.** The list holds both after `create_connect_link`, when it holds any tool. Each acts only for the application user that the URL names. `list_connected_accounts` takes no arguments and answers `items`: the `id`, `toolkit_slug`, `status`, `auth_config_id` and `created_at` of each connected account of that user, with every status. `delete_connected_account` takes `connected_account_id`, deletes that account, and answers `{"id": "<id>", "deleted": true}`. It always asks the provider to revoke the access of the account, as `DELETE /v1/tools/connected_accounts/{id}` does with `revoke_on_delete=true`, and it cannot be undone. An id of another application user, of another project or of no account answers `ActionExecute_ConnectedAccountNotFound`.

### Repeated calls

A client can lose the answer to a call, for example when the connection breaks, and send the call again. SambungAPI does not run a Tool that changes data twice for the same call. The same call is the same Tool, with the same arguments, from the same project key, for the same application user. If it arrives while the first call still runs, SambungAPI waits up to 20 seconds for the first call to end; if the first call still runs after that, the answer is an error that says so. If it arrives within 60 seconds after the first call ended, SambungAPI answers with the result of the first call, marked `replayed: true`, and does not run the Tool again. A repeated call costs no usage unit. A Tool that only reads data always runs.

## What the MCP endpoint does not do

Some parts of the [data plane](/glossary#glossary-term-data-plane) have no MCP tool. An agent that can send HTTP requests uses the REST route with the same project key. A host that runs tool calls only, such as the Anthropic Messages API, cannot use them.

| What you want to do | REST route |
| --- | --- |
| List the files of the project | `GET /v1/tools/files/list` |
| Get a new connect link for an account that is not waiting | `POST /v1/tools/connected_accounts/{id}/refresh` |
| Create an auth config with your own OAuth application | `POST /v1/tools/auth_configs` |
| List or read the auth configs of the organization | `GET /v1/tools/auth_configs` and `GET /v1/tools/auth_configs/{id}` |
| Delete an auth config of the organization | `DELETE /v1/tools/auth_configs/{id}` |

## Upload a file

A tool that takes a file, such as `GOOGLEDRIVE_UPLOAD_FILE`, reads the bytes from a file that you upload first. The list holds the tool `request_file_upload` when it holds a tool that takes a file.

1. Call `request_file_upload` with `filename`, `mimetype` and `md5`, the hex MD5 checksum of the bytes. Its optional `tool` names the tool that takes the file; without it, the file is for `GOOGLEDRIVE_UPLOAD_FILE`. It answers `s3key`, `upload_url` and `upload_url_expires_at`.
2. `PUT` the bytes of the file to `upload_url` with an HTTP client, before `upload_url_expires_at`.
3. Call `GOOGLEDRIVE_UPLOAD_FILE` with `file_to_upload` `{name, mimetype, s3key}`.

A host that runs tool calls only cannot PUT the bytes. It can create a text file with `GOOGLEDRIVE_CREATE_FILE_FROM_TEXT`.

## What a call costs

Each accepted tool call costs one Usage unit, as the execute call does, and writes one row to the project's Execution log, with the path `/v1/tools/{slug}/execute#mcp`. A row that was written before the execute route moved keeps the path of the old route, with `#mcp` at its end, and the console still reads it.

- `tools/list`, `server/discover` and `initialize` cost nothing.
- `create_connect_link` costs nothing and writes no Execution log row.
- list_connected_accounts, delete_connected_account and request_file_upload cost nothing and write no Execution log row.
- A call that is refused before it runs costs nothing: arguments that do not match the input schema, no connected account, or a project past its daily quota (`Quota_Exceeded`).
- A repeated call that gets the first call's result costs nothing.

A provider answer is the tool result: `structuredContent` holds the same `{data, error, successful}` object as the `data` of the execute call, and `isError` is `true` when `successful` is `false`. A refusal is a tool error whose `structuredContent` is the error object of the API, with its `slug` and `suggested_fix`.

## Examples

Each example below connects one client to the endpoint, for the application user `user-123`. Replace `user-123` with the `user_id` of your application user, and `ak_xxxxxxxxxxxxxxxx` with your project key. The shapes come from the server code and from each client's documentation. They were not run against `sabi.metatech.id`, so each one says "shape verified; not executed".

### Claude Code

The project key page in the console shows this command with a new key filled in, under the first-call curl.

```bash
# shape verified; not executed
claude mcp add --transport http sambungapi \
  "https://sabi.metatech.id/mcp?user_id=user-123" \
  --header "Authorization: Bearer ak_xxxxxxxxxxxxxxxx"
```

### `.mcp.json`

A `.mcp.json` file at the root of a repository shares the server with everyone who works there. Keep the key out of the file: Claude Code reads `${SAMBUNGAPI_PROJECT_KEY}` from the environment. Shape verified; not executed.

```json
{
  "mcpServers": {
    "sambungapi": {
      "type": "http",
      "url": "https://sabi.metatech.id/mcp?user_id=user-123",
      "headers": {
        "Authorization": "Bearer ${SAMBUNGAPI_PROJECT_KEY}"
      }
    }
  }
}
```

### Cursor

Cursor reads `.cursor/mcp.json` in a project, or `~/.cursor/mcp.json` for every project, and reads `${env:SAMBUNGAPI_PROJECT_KEY}` from the environment. Shape verified; not executed.

```json
{
  "mcpServers": {
    "sambungapi": {
      "url": "https://sabi.metatech.id/mcp?user_id=user-123",
      "headers": {
        "Authorization": "Bearer ${env:SAMBUNGAPI_PROJECT_KEY}"
      }
    }
  }
}
```

### The Anthropic Messages API

The MCP connector sends `authorization_token` as `Authorization: Bearer`, which the endpoint accepts. The connector supports tool calls only, so the agent connects an account with `create_connect_link`.

```bash
# shape verified; not executed
curl -sS https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "anthropic-beta: mcp-client-2025-11-20" \
  -H "content-type: application/json" \
  -d '{
        "model": "claude-opus-5-5",
        "max_tokens": 4096,
        "mcp_servers": [{
          "type": "url",
          "url": "https://sabi.metatech.id/mcp?user_id=user-123",
          "name": "sambungapi",
          "authorization_token": "ak_xxxxxxxxxxxxxxxx"
        }],
        "tools": [{"type": "mcp_toolset", "mcp_server_name": "sambungapi"}],
        "messages": [{"role": "user", "content": "List my next three calendar events."}]
      }'
```

### The OpenAI Responses API

The `mcp` tool sends `authorization` as `Authorization: Bearer`, which the endpoint accepts.

```bash
# shape verified; not executed
curl -sS https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "content-type: application/json" \
  -d '{
        "model": "gpt-5",
        "tools": [{
          "type": "mcp",
          "server_label": "sambungapi",
          "server_url": "https://sabi.metatech.id/mcp?user_id=user-123",
          "authorization": "ak_xxxxxxxxxxxxxxxx",
          "require_approval": "never"
        }],
        "input": "List my next three calendar events."
      }'
```

## Where the contract is

`GET /v1/specs/tools` carries an `mcp` object: the endpoint, the transport, the protocol revisions, the credential, the application user and this page. Where this page and the specs disagree, the specs are right.
