---
title: Direct MCP
editUrl: https://github.com/Cratis/cli/edit/main/Documentation/reference/direct-mcp.md
description: Run the stdio bridge to Direct's MCP server and register it in AI clients without touching unrelated configuration.
lastUpdated: 2026-10-07T00:00:00.000Z
---


`cratis direct mcp` is a stdio Model Context Protocol (MCP) server that forwards JSON-RPC to Direct's remote MCP endpoint (`<origin>/mcp`) using the credential stored by [`cratis direct login`](/cli/direct/). The bridge defines no tools of its own; every tool comes from Direct and acts with your identity.

## The bridge

```bash
cratis direct mcp [--url <ORIGIN>] [--tenant <TENANT> | --no-tenant]
```

The origin, tenant and stored credential are resolved once, when the bridge starts. Without options the bridge uses the active Direct login, tenant included. `--url`, `--tenant` and `--no-tenant` select another stored login instead, and once any of them is given the active login's tenant is never inherited: `--url` alone means no tenant, and `--no-tenant` means no tenant on the active origin. After logout clears the active selection, an invocation without options requires a new login or `cratis direct use`; it never silently selects a different stored login. Explicitly pinned stored logins remain usable. If no stored login serves the target, the bridge exits with the `cratis direct login` command to run.

Only JSON-RPC is written to stdout; the target it forwards to and failures are written to stderr. An expiring access token is refreshed before a request, and a token Direct rejects is refreshed once; refreshing rotates the stored refresh token. When the refreshed token is also rejected, the bridge answers the request with an error that asks you to log in again.

Every request gets exactly one answer unless the client cancels it with `notifications/cancelled`, which also stops the request to Direct. When Direct's reply does not contain the answer, for example an HTTP error, an empty or unreadable body, an error without the request's id, or an event stream that ends early, the bridge answers with a JSON-RPC error that keeps Direct's error code and message where there is one. The same holds when forwarding fails unexpectedly (an Internal Error, with the details on stderr), and when the bridge stops with requests still in flight because standard input ended or failed or the bridge was cancelled: each of those requests is answered with an error before the bridge exits. When Direct no longer knows the MCP session (HTTP 404), the request fails with an error asking you to reconnect or restart the server; the client's next `initialize` starts a new session. JSON-RPC batches are answered with an Invalid Request error for each request in them, because MCP clients send one message per line. A single message from Direct larger than 32 MiB is refused rather than held in memory. There is no time limit on a request, since tool calls may run for as long as the client is willing to wait.

## Registering the bridge in AI clients

```bash
cratis direct mcp install [--client <CLIENTS>] [--scope user|project] [--url <ORIGIN>] [--tenant <TENANT> | --no-tenant] [--dry-run]
cratis direct mcp status [--client <CLIENTS>] [--scope user|project]
cratis direct mcp uninstall [--client <CLIENTS>] [--scope user|project] [--dry-run]
```

`install` writes one stdio server entry named `cratis-direct` that runs `cratis direct mcp --url <origin> --tenant <tenant>`, or `cratis direct mcp --url <origin> --no-tenant` when there is no tenant. Both are always pinned, so a later `cratis direct use` does not redirect an existing registration; run `install` again to move it. Without options the origin and tenant are those of the active Direct login. `--url`, `--tenant` and `--no-tenant` choose them the same way they do for the bridge, so `--url` without a tenant option pins no tenant. A stored login for the pinned origin and tenant is required.

`--client` takes a comma-separated list of `claude`, `codex`, `copilot`, `cursor`, `opencode` and `pi`. Without it, `install` registers every client whose configuration file or directory already exists in the scope, `uninstall` removes every registration it recorded in the scope, and `status` reports every client. `--scope user` (the default) writes your own client configuration for every project; `--scope project` writes the current directory's configuration, which is shared with everyone using the repository.

| Client | User scope | Project scope | Member |
| --- | --- | --- | --- |
| Claude Code | `~/.claude.json` | `.mcp.json` | `mcpServers.cratis-direct` |
| Codex | `~/.codex/config.toml`, or `$CODEX_HOME/config.toml` | `.codex/config.toml` | `[mcp_servers.cratis-direct]` |
| Copilot in VS Code | VS Code's user `mcp.json`: `~/Library/Application Support/Code/User/` on macOS, `%APPDATA%\Code\User\` on Windows, `$XDG_CONFIG_HOME/Code/User/` (default `~/.config/Code/User/`) on Linux | `.vscode/mcp.json` | `servers.cratis-direct` |
| Cursor | `~/.cursor/mcp.json` | `.cursor/mcp.json` | `mcpServers.cratis-direct` |
| OpenCode | `~/.config/opencode/opencode.json` (or `$XDG_CONFIG_HOME/opencode/`), or an existing `opencode.jsonc` | `opencode.json`, or an existing `opencode.jsonc` | `mcp.cratis-direct` |

Some registrations are reported as unsupported instead of guessed, and nothing is written for them:

- **Pi** has no native MCP configuration file. Load `cratis direct mcp` through a Pi MCP extension.
- **Claude Code with `CLAUDE_CONFIG_DIR` set** keeps its user configuration elsewhere. Use `claude mcp add --scope user` instead.
- **A configuration directory relocated outside your home directory** by `CODEX_HOME`, `XDG_CONFIG_HOME` or `APPDATA` is not written, because ownership is recorded relative to the home directory.
- **Copilot** means the VS Code integration. VS Code Insiders, non-default VS Code profiles and the standalone Copilot CLI are not registered.

`install` fails without changing anything when none of the selected clients can be registered, or when it finds no client configuration and `--client` is not given.

## Ownership and safety

Only the `cratis-direct` member is written or removed. Other servers and settings, formatting, JSONC comments and unrelated TOML stay as they are. Each member written is recorded in `.cratis/direct-mcp.json` in the scope's root: your home directory for the user scope, the project for the project scope. The record is removed once nothing is owned.

- An existing `cratis-direct` entry that is not in the record belongs to you. `install` reports it as a conflict and changes nothing, in any client.
- An owned entry that was changed after it was installed is left alone: `install` and `uninstall` report it as a conflict and change nothing. Restore or remove the entry yourself first.
- An owned entry you removed from the client configuration is treated as absent: `install` adds it again, `uninstall` only forgets it, and `status` reports it as `absent`.
- Before changing an existing client configuration, `install` and `uninstall` save its original bytes beside it as `<filename>.<unique-id>.bak`. Unix backups keep the original owner and group but are restricted to `0600`, with extended and inherited ACLs removed and verified before content is copied. Windows backups retain every original effective DACL entry, converting inherited entries to explicit entries and protecting the copied DACL from broader directory inheritance before creation. Protection is verified before copying content; Windows assigns owner/group. If backup protection cannot be established, the partial backup is removed and neither the client nor ownership record is changed. Backups never overwrite earlier backups. An unchanged repeated install and a dry run create no backups.
- Existing client files are opened without following links, checked for the original file identity and exact bytes through that same handle, then rewritten in place under the scope lock, flushed to disk and truncated to the new length, retaining their exact inode, ACL, owner, group and mode. Only absent client files use temporary-file creation and rename. This protection policy is scoped to Direct MCP registration; other AI writes retain their existing behavior.
- Commands applying changes in the same scope cannot overlap. A busy scope or an ownership record changed since planning is rejected before changing client files; run the command again. Scope locks are reused in `~/.cratis/direct-mcp-locks/`, keyed by the scope's physical path, not left in project directories. An empty uninstall or an already up-to-date install creates no lock or other files.
- Running `install` again with the same origin and tenant changes nothing. With a different origin or tenant, it updates only the entries it owns.
- A registration stays where it was written when `CODEX_HOME`, `XDG_CONFIG_HOME`, `APPDATA` or `CLAUDE_CONFIG_DIR` changes later. `uninstall` and `status` act on the recorded file even when the client now reads another location or can no longer be registered; `install` moves an unchanged registration to the client's current location, and leaves it where it is when the client can no longer be registered.
- A record that names a file or member this command could not have written, under any of those settings, is rejected before anything is changed: a path outside the scope's root, or one that does not end in the client's own configuration file.
- `--dry-run` prints each member that would be added, updated or removed, with its exact value in the file's own format (JSON, or the TOML table for Codex), and writes nothing.
- Symbolic links in the configuration path are refused, as are malformed or duplicate-property JSON and TOML shapes that cannot be edited without rewriting your content. A file changed by another process while the command runs is not overwritten; run the command again.
- Ownership is published after each client file is written successfully, never before backup protection is established. If a later client file fails, the next run still owns the earlier completed writes and can finish the rest. An interruption during a single in-place rewrite, or between a file write and its ownership publication, can require restoring that file's backup before retrying; the command does not guess ownership of an incomplete or unrecorded write.

### Recovering an interrupted rewrite

If an in-place write may have changed bytes before an I/O failure, its error names the configuration and the exact backup to restore. A refusal that says nothing was written does not require restoration: review the current file and retry without overwriting concurrent changes. After an abrupt interruption without an error message, find backups beside that configuration matching `<filename>.<unique-id>.bak`. The IDs are random, not timestamps: use the backup's last-modified time to identify the newest backup from the interrupted command. Keep earlier backups until recovery is complete.

Stop the client and any registration commands before restoring. First verify that every path component is free of symbolic links and the configuration still names the intended original regular file; do not restore over a concurrent edit or a replacement file. Copy the backup's **content into the existing configuration file** to keep that file's inode, owner, group, mode and ACL. On Unix, after that verification and while the directory is trusted and cannot be changed by another process, for example:

```bash
cat -- /path/to/config.json.<unique-id>.bak > /path/to/config.json
```

On Windows, copy the bytes into an opened existing file without deleting it first. Do not rename or move the backup over the configuration, or delete and recreate the configuration: that replaces the original inode and its permissions with the backup's protection. Confirm the configuration is still the intended regular file, not a symbolic link, before restoring; then retry the command.

Filesystems that return `EOPNOTSUPP` (errno 95) for POSIX ACL inspection are refused rather than assumed ACL-free, because another ACL model may still grant access. The error distinguishes backup protection from new-client-file protection and reports the errno. Create the client configuration manually or move it to a filesystem supporting ACL inspection.

`status` reads only. Per client it reports `registered` (with the command it launches), `modified`, `user-owned`, `absent` or `unsupported`, and the configuration path. For a registration it owns, that is the recorded path, and `registered` also says when the client now reads another file. Clients still apply their own trust and approval prompts, and usually need a restart to load a new server; registration does not claim that a client has connected.

Verified native references: [Claude Code MCP](https://code.claude.com/docs/en/mcp), [VS Code MCP configuration](https://code.visualstudio.com/docs/copilot/reference/mcp-configuration), [Cursor MCP](https://cursor.com/docs/context/mcp), [OpenCode configuration](https://opencode.ai/docs/config/), and [Codex MCP](https://developers.openai.com/codex/mcp/).
