# MCP server and tool registry

> Reference: Remote MCP endpoint, OAuth-backed server, toolset composition, SendEmail boundary, generated schemas, and docs tool pages.

- Repository: macro-inc/macro
- GitHub: https://github.com/macro-inc/macro
- Human docs: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e
- Complete Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/llms-full.txt

## Source Files

- `docs/AI/mcp/overview.mdx`
- `docs/AI/mcp/tools/index.mdx`
- `rust/cloud-storage/mcp_service/src/main.rs`
- `rust/cloud-storage/mcp_service/src/tool_service.rs`
- `rust/cloud-storage/ai_tools/src/lib.rs`
- `docs/scripts/generate-mcp-tool-pages.ts`

---

---
title: "MCP server and tool registry"
description: "Reference: Remote MCP endpoint, OAuth-backed server, toolset composition, SendEmail boundary, generated schemas, and docs tool pages."
---

Macro exposes a remote Streamable HTTP MCP endpoint at `https://mcp-server.macro.com/mcp`; the Rust `mcp_service` binary mounts the protected MCP service at `/mcp`, brokers OAuth through FusionAuth-backed routes, builds the runtime registry from `ai_tools::mcp_tools()`, and executes tool calls with the authenticated Macro user id.

## Remote endpoint

```text
https://mcp-server.macro.com/mcp
```

The public docs configure clients as an HTTP MCP server; no local Macro process is required.

<CodeGroup>

```bash title="Claude Code"
claude mcp add --transport http macro https://mcp-server.macro.com/mcp
```

```bash title="Codex CLI"
codex mcp add macro --url https://mcp-server.macro.com/mcp
```

```json title="Generic MCP JSON config"
{
  "mcpServers": {
    "macro": {
      "type": "http",
      "url": "https://mcp-server.macro.com/mcp"
    }
  }
}
```

</CodeGroup>

At runtime, `mcp_service` binds `0.0.0.0:${PORT}` with `PORT=8090` as the default. The server config enables JSON responses, disables stateful mode, and allows the host from `MCP_PUBLIC_URL` plus `localhost` and `127.0.0.1`.

```text
Client
  ├─ OAuth discovery/register/authorize/token ──> mcp_auth_proxy routes
  └─ Bearer-authenticated MCP calls /mcp ───────> AuthenticatedToolService
                                                    └─ ai_tools::mcp_tools()
                                                       └─ Macro domain services
```

## HTTP routes

| Route | Auth | Purpose |
| --- | --- | --- |
| `GET /health` | No bearer token | ALB health check returning `ok`. |
| `GET /.well-known/oauth-protected-resource` | No bearer token | OAuth protected-resource metadata. |
| `GET /.well-known/oauth-protected-resource/mcp` | No bearer token | Protected-resource metadata for MCP clients. |
| `GET /mcp/.well-known/oauth-protected-resource` | No bearer token | Alternate protected-resource discovery path. |
| `GET /.well-known/oauth-authorization-server` | No bearer token | OAuth authorization-server metadata. |
| `GET /.well-known/oauth-authorization-server/mcp` | No bearer token | Authorization-server metadata for MCP clients. |
| `GET /mcp/.well-known/oauth-authorization-server` | No bearer token | Alternate authorization-server discovery path. |
| `POST /register` | No bearer token | Dynamic public-client registration. |
| `GET /authorize` | No bearer token | Starts an OAuth authorization-code + PKCE flow. |
| `GET /oauth/callback` | No bearer token | Receives the upstream FusionAuth callback. |
| `POST /token` | No bearer token | Exchanges broker-issued codes or refresh tokens. |
| `/mcp` | Bearer token required | Streamable HTTP MCP service. |

CORS is applied outside the bearer middleware so browser MCP clients can complete OAuth preflights and still receive `WWW-Authenticate` challenges. Allowed methods are `GET`, `POST`, `DELETE`, and `OPTIONS`; allowed headers include `Authorization`, `Content-Type`, `mcp-protocol-version`, and `mcp-session-id`.

## OAuth and access control

The MCP auth proxy presents itself as the OAuth authorization server to MCP clients while delegating the upstream sign-in flow to FusionAuth.

### OAuth behavior

| Behavior | Value |
| --- | --- |
| Supported `response_type` | `code` |
| Supported grant types | `authorization_code`, `refresh_token` |
| Supported PKCE method | `S256` |
| Dynamic client auth method | `none` |
| Pending authorization TTL | 10 minutes |
| Broker-issued authorization code TTL | 5 minutes |
| Redirect URI policy | `https` or loopback `http` for `localhost`, `127.0.0.1`, or `[::1]` |
| Upstream provider | FusionAuth OAuth provider using the `google_gmail` identity provider |
| In-flight state store | Redis keys under `mcp_auth_proxy:pending:*` and `mcp_auth_proxy:issued:*` |

The `/mcp` route validates `Authorization: Bearer <token>` with Macro JWT validation. Both Macro access tokens and Macro API tokens can supply the `macro_user_id`; the middleware stores that value as `MacroUserIdStr` in request extensions for the MCP service.

### Subscription gate

`list_tools` and `call_tool` both require the authenticated user to have at least one paid AI permission:

- `write:opus`
- `write:sonnet`
- `write:haiku`

If none are present, the MCP service returns an MCP invalid-request error with:

```text
MCP access requires a paid subscription
```

## Tool service contract

`AuthenticatedToolService` implements `rmcp::handler::server::ServerHandler`.

| MCP method | Runtime behavior |
| --- | --- |
| `get_info` | Advertises `macro-tools`, title `Macro`, package version, tool capability, and instructions for searching, reading, creating documents, and listing entities. |
| `list_tools` | Extracts the authenticated user, checks paid access, and returns every tool in `toolset.tools` with its name, description, and input schema. |
| `call_tool` | Extracts the authenticated user, checks paid access, requires object arguments, then invokes `toolset.try_tool_call(...)` with `RequestContext { user_id }`. |

### Call results and errors

| Case | MCP response |
| --- | --- |
| Missing `arguments` | Invalid params: `No params provided`. |
| Tool input deserialization fails | Parse error with the deserialization message. |
| Tool name is not registered | Resource-not-found error. |
| Tool returns `Ok(value)` | Structured MCP tool result. |
| Tool returns a domain `ToolCallError` | MCP tool-result error containing the user-facing error description as text. |
| Missing user id in request extensions | Internal error: `missing user identity — is auth configured?`. |

## Runtime toolset composition

The MCP runtime registry is constructed in `ai_tools::mcp_tools()`, not from the checked-in MDX files.

| Function | Composition |
| --- | --- |
| `subagent_toolset()` | Search tools, `ListEntities`, document tools, property tools, call tools, chat tools, channel tools, team tools, and Anthropic server-tool wrappers. It intentionally excludes email and the `Subagent` tool itself. |
| `all_tools()` | `subagent_toolset()` plus notification tools, the full email toolset, and `Subagent`. This is the broader AI-provider-facing registry. |
| `mcp_tools()` | `subagent_toolset()` plus notification tools, the MCP email toolset, and `Subagent`. This is what `mcp_service` exposes. |
| `no_tools()` | Empty toolset with the base prompt. |

<Note>
The registry boundary is provider-neutral at the MCP layer: tools are composed as Rust `AsyncToolCollection` entries. The current registry includes a subtoolset for Anthropic server-tool wrappers, but the MCP server contract is the tool registry and Streamable HTTP transport, not a requirement that every deployment use one model provider.
</Note>

## SendEmail boundary

`SendEmail` is deliberately outside the MCP email toolset.

| Email registry | Includes |
| --- | --- |
| `email::inbound::toolset::mcp_toolset()` | `UpdateThreadLabels`, `GetThread`, `ListLabels` |
| `email::inbound::toolset::email_toolset()` | Everything in `mcp_toolset()` plus `SendEmail` as a user tool |
| `ai_tools::mcp_tools()` | Uses `email_mcp_toolset()`; therefore excludes direct `SendEmail` execution |
| `ai_tools::all_tools()` | Uses the full `email_toolset()` |

`SendEmail` composes and sends an email by resolving the user’s linked email account, creating message input from `subject`, Markdown/HTML body, recipients, and optional `replyingToId`, then calling the email service. In the full AI tool registry it is registered through `add_user_tool`, whose wrapper returns `PendingUserExecution` when called by the automatic tool loop. That user-action boundary is not exposed through the MCP runtime registry.

<Warning>
The checked-in generated MCP tool pages currently include a `SendEmail` page. Treat the live MCP `list_tools` response and `ai_tools::mcp_tools()` as the runtime source of truth.
</Warning>

## Generated schemas and docs tool pages

The docs generator lives in `docs/scripts/generate-mcp-tool-pages.ts` and is wired through the docs package scripts.

```json title="docs/package.json"
{
  "scripts": {
    "generate:tools": "bun ./scripts/generate-mcp-tool-pages.ts",
    "prepare": "bun run generate:tools"
  }
}
```

The generator workflow is:

<Steps>
<Step title="Build the Rust schema binary">
It runs `SQLX_OFFLINE=true cargo build --bin gen_tool_schemas` from `rust/cloud-storage`.
</Step>

<Step title="Regenerate the schema JSON">
It removes `rust/cloud-storage/ai_tools/schemas`, then runs the `gen_tool_schemas` binary from the `ai_tools` directory. The binary writes `schemas/tools.json`.
</Step>

<Step title="Write MDX pages">
It rewrites `docs/AI/mcp/tools/*.mdx`, including `index.mdx`.
</Step>

<Step title="Update navigation">
It writes `docs/config/tool-pages.json`, which is referenced by `docs/config/navigation.json` under the MCP Tool Reference group.
</Step>
</Steps>

The Rust schema binary currently serializes `ai_tools::all_tool_combined_schema()`, which merges the broad `all_tools()` registry plus a schema-only `ReadThread` phantom tool into a combined schema with shared `$defs` and `tools` entries.

<Warning>
The TypeScript docs script expects a `schemas` array with `name`, `description`, `inputSchema`, and `outputSchema`, while the Rust binary emits the combined schema shape `{ "$defs": ..., "tools": [...] }`. Keep the Rust schema shape and docs script contract aligned before relying on `bun run generate:tools` in CI or release automation.
</Warning>

## Configuration

`McpEnvVars` loads these required environment variables during context construction. `PORT` is read separately and defaults to `8090`.

| Environment variable | Used for |
| --- | --- |
| `DATABASE_URL` | Postgres connection pool for tools, permissions, and domain services. |
| `EMAIL_SCHEDULED_QUEUE` | SQS email scheduling queue configuration. |
| `DOCUMENT_STORAGE_SERVICE_URL` | Search/document client base URL wiring. |
| `SYNC_SERVICE_URL` | Sync service client base URL. |
| `SYNC_SERVICE_AUTH_KEY` | Secret-manager key for sync service authentication. |
| `LEXICAL_SERVICE_URL` | Lexical client base URL. |
| `EMAIL_SERVICE_URL` | Email service client base URL. |
| `STATIC_FILE_SERVICE_URL` | Loaded as part of MCP environment configuration. |
| `DOCUMENT_STORAGE_BUCKET` | Document S3 upload adapter bucket. |
| `DOCX_DOCUMENT_UPLOAD_BUCKET` | DOCX upload bucket. |
| `DOCUMENT_STORAGE_SERVICE_CLOUDFRONT_DISTRIBUTION_URL` | CloudFront document distribution URL. |
| `DOCUMENT_STORAGE_SERVICE_CLOUDFRONT_SIGNER_PUBLIC_KEY_ID` | CloudFront signer public key id. |
| `DOCUMENT_STORAGE_SERVICE_CLOUDFRONT_SIGNER_PRIVATE_KEY_SECRET_NAME` | Secret-manager key for the CloudFront private key. |
| `MCP_PUBLIC_URL` | Public base URL for OAuth metadata, callback URL construction, and allowed host derivation. |
| `FUSIONAUTH_BASE_URL` | FusionAuth base URL. |
| `FUSIONAUTH_CLIENT_ID` | FusionAuth OAuth client id. |
| `FUSIONAUTH_TENANT_ID` | FusionAuth tenant id. |
| `FUSIONAUTH_API_KEY_SECRET_KEY` | Secret-manager key for FusionAuth API access. |
| `FUSIONAUTH_CLIENT_SECRET_KEY` | Secret-manager key for the FusionAuth client secret. |
| `GOOGLE_CLIENT_ID` | Google OAuth client id passed to FusionAuth client setup. |
| `GOOGLE_CLIENT_SECRET_KEY` | Secret-manager key for the Google client secret. |
| `REDIS_URL` | Redis connection string for in-flight OAuth state. |
| `PORT` | Optional listen port; defaults to `8090`. |

## Adding or changing MCP tools

1. Implement the tool as an `AsyncTool` with `Deserialize` input and `Serialize + JsonSchema` output.
2. Register it in the narrow domain toolset first.
3. Wire that domain toolset into `ai_tools::mcp_tools()` only if it should be exposed over remote MCP.
4. Add or update `FromRef<ToolServiceContext>` wiring and `build_context()` dependencies when the tool needs a new service.
5. Keep `email::mcp_toolset()` separate from `email_toolset()` if the operation requires user review or should not be remotely executable.
6. Regenerate docs with `cd docs && bun run generate:tools` after fixing or confirming the schema contract.
7. Verify the live runtime with an authenticated MCP `list_tools` call; do not use checked-in MDX pages as the only availability signal.

## Troubleshooting

| Symptom | Likely cause | Verification |
| --- | --- | --- |
| `401` with `WWW-Authenticate` and no `error` | Missing bearer token on `/mcp`. | Check the client completed OAuth and sends `Authorization: Bearer ...`. |
| `401` with `error="invalid_token"` | JWT validation failed or token user id was invalid. | Re-run the OAuth flow or inspect token issuer/configuration. |
| `redirect_uri must be https or a loopback address` | OAuth client used a non-HTTPS, non-loopback redirect URI. | Use HTTPS or `http://localhost`, `http://127.0.0.1`, or `http://[::1]`. |
| `unsupported code_challenge_method` | Client did not use PKCE `S256`. | Confirm MCP client OAuth settings. |
| `invalid or expired code` | Broker-issued code was already used or older than 5 minutes. | Restart the OAuth flow. |
| `MCP access requires a paid subscription` | User lacks `write:opus`, `write:sonnet`, and `write:haiku`. | Check user permissions in Macro. |
| `No params provided` | `call_tool` request omitted arguments. | Send an object in MCP tool arguments, even when the tool has few parameters. |
| Tool page exists but tool is absent from MCP | Generated docs are broader or stale relative to `mcp_tools()`. | Compare against authenticated `list_tools`. |

## Related pages

<CardGroup>
<Card title="MCP setup" href="/AI/mcp/overview">
Client connection commands and the public remote endpoint.
</Card>
<Card title="MCP tool reference" href="/AI/mcp/tools">
Generated tool pages and navigation for documented tool schemas.
</Card>
</CardGroup>
