# One MCP endpoint for every tool

connectable.sh is an MCP gateway. Add it to Claude Code, Codex or Cursor as one server, and your agent gets your MCP servers, APIs, databases and web pages as tools, with policy, rate limits, approvals and an audit log in front. Built on the [Model Context Protocol](https://modelcontextprotocol.io) by [linesofcode](https://x.com/linesofcode).

Install: `npm install -g connectable.sh` · Currently v0.1.0 beta

## The support-desk example

You write:

```json
{
	"connectors": [
		{
			"name": "support",
			"transport": "driver",
			"driver": "sqlite",
			"options": {
				"path": "./support.db",
				"maxRows": 100,
				"instructions": "tickets.status is one of open, pending, closed. Join tickets.customer_id to customers.id for the plan."
			}
		},
		{
			"name": "guides",
			"transport": "driver",
			"driver": "filesystem",
			"options": { "roots": ["./guides"] }
		}
	]
}
```

Your agent gets:

```
tools: support__list_tables, support__describe_table, support__query, guides__list_directory, guides__read_file, guides__search_files, guides__stat

open tickets:
| id | name | plan | subject |
| --- | --- | --- | --- |
| 1 | Acme Corp | team | SSO login loops back to sign-in |
| 4 | Initech | enterprise | Rate limit hit on bulk import |
| 5 | Initech | enterprise | Request: SCIM provisioning |

3 rows

guide:
# SSO login loop
...

write attempt (isError=true):
only read statements are allowed (SELECT, WITH, VALUES, EXPLAIN); got "UPDATE"
```

## How a call travels

Your client connects to one URL with one key. The gateway checks each call against the endpoint that key belongs to, dials the upstream with credentials the agent never sees, and writes the call to the audit log.

In front of every call: allow and deny lists, rate limits, spend caps, human approval, secrets store, audit log.

1. **Describe your tools.** List connectors in `connectors.json`, or add them in the dashboard. `connectable check` validates the file before anything starts.
2. **Create an endpoint and a key.** An endpoint decides which connectors a key reaches, with its own rate limit and agent instructions. `connectable endpoints` manages them from a terminal.
3. **Add one URL to your client.** `claude mcp add --transport http` with the URL and the key. Tools arrive namespaced, like `files__read_file`.

## Runs where your agents already are

Claude Code, Codex, Cursor, Windsurf, VS Code and opencode on one side. Postgres, MySQL, SQLite, DuckDB, S3, R2 and any MCP server, OpenAPI or GraphQL document on the other, self-hosted on Fly or on Cloudflare.

## Principles

- **Nothing is on by default.** `include` has no default, a webhook with no events receives nothing, and the GraphQL surface ships off.
- **Credentials are names.** Connectors hold `{{ secrets.NAME }}`, exports carry the name, and an echoed value reaches the model as `[secret:NAME]`.
- **Every call on the record.** Each governed call writes a hash-chained audit row, and `connectable audit verify` exits non-zero on a break.

## One file, or one API

Self-hosted, everything lives in `connectors.json`. Hosted, the dashboard calls the same `/api/v1` your scripts do, and every screen prints the calls it makes. The CLI and the TypeScript client sit on that API too.

Connectors:

```json
{
	"connectors": [
		{
			"name": "files",
			"transport": "stdio",
			"command": "npx",
			"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"]
		},
		{
			"name": "sentry",
			"transport": "http",
			"url": "https://mcp.sentry.dev/mcp"
		}
	]
}
```

Playbooks:

```json
{
	"prompts": [
		{
			"name": "weekly_sales",
			"description": "Revenue by region for one week",
			"arguments": [{ "name": "week", "required": true }],
			"template": "Sum revenue by region in lake for {{week}}. Read connectable://endpoint/orders-schema first."
		}
	],
	"resources": [{ "name": "orders-schema", "text": "# Orders\n- order_id …" }],
	"connectors": [ ... ]
}
```

Webhooks:

```ts
import { verifyWebhookSignature } from 'connectable.sh';

const ok = verifyWebhookSignature(
	process.env.WEBHOOK_SECRET,
	req.headers.get('x-connectable-signature'),
	rawBody,
);
```

App bridge:

```ts
import { connect } from 'connectable.sh/client';

const bridge = await connect({ app: 'ohmyascii', relay: '/api/bridge' });
for await (const event of bridge!.ask('a lighthouse in a storm', { system })) {
	if (event.type === 'text') render(event.delta);
}
await bridge!.expose([{ name: 'get_scene', handler: () => scene }]);
```

## Connect what you already have

MCP servers, APIs, databases, web pages and the agents on your laptop, each one a connector with its tools behind the same endpoint.

### Wrap a REST or GraphQL API

Set `transport` to `openapi` or `graphql` and point `specUrl` at the document. Only the operations you list in `include` become tools, and leaving it out gives you none. Learn more in [Wrapping a REST or GraphQL API](/reference#wrapping-a-rest-or-graphql-api).

```json
{
	"connectors": [
		{
			"name": "stripe",
			"transport": "openapi",
			"specUrl": "https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json",
			"headers": { "authorization": "Bearer sk_test_…" },
			"include": ["GetCustomers", "GetCharges", "GetChargesCharge"]
		},
		{
			"name": "linear",
			"transport": "graphql",
			"specUrl": "https://api.linear.app/graphql",
			"headers": { "authorization": "…" },
			"include": ["Query.issues", "Query.issue"]
		}
	]
}
```

### Serve a web page as a tool

A `web` connector turns each page you name into a read-only tool that returns Markdown, narrowed by `selector`. The origin is fixed, so a `{param}` fills a path or query and never the host.

```json
{
	"connectors": [
		{
			"name": "docs",
			"transport": "web",
			"pages": [
				{
					"tool": "changelog",
					"url": "https://bun.com/blog",
					"selector": "main"
				},
				{
					"tool": "wikipedia",
					"url": "https://en.wikipedia.org/wiki/{title}",
					"params": [{ "name": "title", "description": "Article title" }],
					"selector": "#mw-content-text",
					"maxChars": 8000
				}
			]
		}
	]
}
```

### Query a database read-only

Set `"driver"` to `postgres`, `mysql`, `sqlite`, `duckdb` or `s3`. Every SQL driver runs one read-only statement per call with a timeout and a `maxRows` cap that is reported when it bites. `execute` exists only with `allowWrites`.

```json
{
	"connectors": [
		{
			"name": "warehouse",
			"transport": "driver",
			"driver": "postgres",
			"options": {
				"connectionString": "{{ env.WAREHOUSE_URL }}",
				"schemas": ["analytics"],
				"maxRows": 200
			}
		},
		{
			"name": "exports",
			"transport": "driver",
			"driver": "s3",
			"options": {
				"bucket": "acme-exports",
				"prefix": "reports/",
				"accessKeyId": "{{ env.AWS_ACCESS_KEY_ID }}",
				"secretAccessKey": "{{ env.AWS_SECRET_ACCESS_KEY }}"
			}
		}
	]
}
```

### All of MCP, not only tools

Resources, prompts, sampling and elicitation go through the same governed path as `tools/call`. A resource is re-published as `connectable://<connector>/…`, so two connectors never hand out one URI.

| Capability | Through the gateway |
| --- | --- |
| `tools/list`, `tools/call` | Namespaced `connector__tool`. Policy, aliases, pins, approvals, caps, quotas, audit. |
| `outputSchema`, `structuredContent` | Passed through unchanged. Which tools declare one, and why the rest do not: [`output-schemas.md`](output-schemas.md). |
| `resources/*` | Re-published under `connectable://<connector>/<encoded-uri>`. Reads are governed. |
| `prompts/list`, `prompts/get` | Namespaced `connector__prompt`, governed on the same path. |
| `sampling/createMessage` | Forwarded from the upstream to the client whose call provoked it. Needs a deployment that holds sessions; see below. |
| `elicitation/create` | Same. |
| `*/list_changed`, `resources/updated` | Forwarded, so a client re-lists instead of trusting a stale one. |

### Tell the agent what's where

An agent picks tools by name, and `db1__query` says nothing about what `db1` holds. A top-level `instructions` string in `connectors.json` reaches the model at `initialize`. Hosted, each endpoint carries its own.

```json
{
	"instructions": "Orders are in lake (DuckDB, one Parquet file per day under ~/data/orders). Support guides are in notes. Never write to bucket.",
	"connectors": [ ... ]
}
```

### Local tools, hosted gateway

`connectable link pair` connects your laptop to a hosted deployment over an outbound connection, with no open port and no tunnel. The local `connectable.link.json` is final: the gateway can call only what it lists.

```bash
connectable link pair --url https://connectable.sh   # prints a code; approve it in the dashboard
connectable link ./connectable.link.json              # serve what that file describes
```

### Your coding agent as a tool

The `agent` driver turns Claude Code, Codex or opencode in a directory into one tool, `run_task`. `permission` is `plan` or `edit`, `maxBudgetUsd` caps a run, and there is no bypass mode.

```json
{
	"name": "claude-code",
	"transport": "driver",
	"driver": "agent",
	"options": { "runner": "claude", "cwd": "~/code/app", "permission": "plan", "maxBudgetUsd": 2 }
}
```

## Decide what an agent may do

Credentials, limits, approvals and refusals apply to every call, and every call leaves a row you can check.

### Credentials stay in the store

Connectors reference `{{ secrets.NAME }}`, and the value is encrypted with `CREDENTIAL_ENCRYPTION_KEY` and resolved just before the upstream is dialed. Rotating a key shared by three connectors is one `connectable secrets set`.

```bash
connectable secrets set STRIPE_API_KEY sk_live_…
connectable connectors create stripe --transport http --url https://mcp.stripe.test/mcp \
  --header 'Authorization=Bearer {{ secrets.STRIPE_API_KEY }}'
```

### Three rate-limit tiers

Each API key, endpoint and connector has its own limit, and a call is charged against every tier that applies. `0` means no limit at that tier. A call refused by a later tier refunds the earlier ones.

| Tier | Keyed by | Set on | What it is for |
| --- | --- | --- | --- |
| **Connection** | API key | Each key, under **Endpoints → Keys** | Throttle one misbehaving client without touching the others |
| **Endpoint** | Endpoint | The endpoint's settings | The customer quota: fairness and cost control across all its keys |
| **Connector** | Connector | The connector's settings | Self-protection: we will not overrun an upstream, whoever is asking |

### Approvals reach a person

Subscribe a URL to `approval.requested` and a waiting call reaches whoever can decide it. Each delivery is signed with `x-connectable-signature` and carries names and ids, never arguments or credentials.

```bash
connectable webhooks create pagerduty https://hooks.example.com/connectable \
  --events approval.requested,approval.expired,kill_switch.engaged
# secret whsec_…   (shown once — store it where the receiver can read it)
connectable webhooks ping $WEBHOOK_ID          # one delivery, right now
connectable webhooks deliveries $WEBHOOK_ID    # what was sent, and what came back
```

### Refusals an agent can read

Every error carries `data.category` (`policy`, `quota`, `auth`, `upstream`, `gateway` or `request`) and a conservative `retryable`. An agent tells "wait" from "never" without parsing English.

```json
{
	"code": -32603,
	"message": "rate limit exceeded for this endpoint (60/min); retry in 12s",
	"data": {
		"category": "quota",
		"code": "rate_limited",
		"retryable": true,
		"detail": { "scope": "endpoint", "retryAfterMs": 12000, "limit": 60 }
	}
}
```

### An audit log you can check

Rows are hash-chained, each one hashed with the row before it. Store the head from `connectable audit chain` somewhere this deployment does not control, and `connectable audit verify` fails on an edit, a gap or a cut from the end.

```bash
connectable audit chain      # the current head — record this somewhere else
connectable audit verify     # re-derive the whole chain; exits non-zero on any break
```

## Run it hosted or yourself

Point any MCP client at it, use the hosted dashboard, or run the gateway on your own machine and keep the dashboard.

### Connect any MCP client

A client gets one URL and one key. The dashboard prints these per client under **Connect a client**, and `connectable import-mcp` reads the servers you already have configured.

Claude Code:

```bash
claude mcp add --transport http connectable http://localhost:3333/mcp \
  --header "Authorization: Bearer $CONNECTABLE_API_KEY"
```

Codex:

```bash
# Codex reads the key from the environment
codex mcp add connectable --url http://localhost:3333/mcp --bearer-token-env-var CONNECTABLE_API_KEY
```

Cursor:

```json
{
	"mcpServers": {
		"connectable": {
			"type": "http",
			"url": "http://localhost:3333/mcp",
			"headers": { "Authorization": "Bearer <your-api-key>" }
		}
	}
}
```

Already set up servers in Claude Desktop or Cursor? Run `connectable import-mcp claude-desktop` and they land in `connectors.json`, with credentials lifted into `.env.connectable`.

### Run the gateway yourself

`connectable data-plane` runs the gateway on your machine and keeps the hosted dashboard. Config comes down without a credential, and audit heads go up signed by a key from `connectable keygen` that never leaves your host.

```bash
# On the control plane, once:
connectable deployments create prod-gateway        # prints a cbd_… token, shown once

# On your own machine, with the self-host binary (One binary, below):
connectable keygen > deployment.key        # Ed25519, never leaves this host
export CONNECTABLE_DEPLOYMENT_TOKEN=cbd_…
connectable data-plane --url https://app.connectable.sh --key deployment.key
```

### Plans for the hosted dashboard

Hosted plans are a flat monthly base with seats above it. Calls are unlimited and never charged for on any plan, and `stdio` connectors start at Team.

| Plan | Price | Seats | Connectors, endpoints, agents, calls | Retention | stdio |
| --- | --- | --- | --- | --- | --- |
| Free | $0 | 1 | 5 connectors, 2 endpoints, unlimited calls and agents | 1 day | no |
| Team | $50/mo, 5 seats included, then $10/seat | unlimited | unlimited | 180 days | yes |
| Enterprise | Same list price, negotiated terms | unlimited | unlimited | unlimited | yes |

## Measured against real specs

We ran `measure-openapi-fidelity.ts` against Stripe, GitHub, Slack, DigitalOcean and our own spec. A generated API tool costs more context than a typical MCP tool, which is why `include` has no default.

- 2175 operations measured
- 93% clean schema rate
- 306 tokens per API tool
- 214 tokens per MCP tool, for comparison

```
$ bun packages/core/scripts/measure-openapi-fidelity.ts
--- corpus totals ---

operations measured    2175
clean schema rate      93%
operationId coverage   93%
read-only (retryable + cacheable)  49%
tokens/tool            306  (vs 214 for MCP tools)
description budget saves 12% of the untrimmed cost
```

## Start with the example

`examples/support-desk` is a SQLite database and a folder of guides behind one gateway. It runs offline with no credentials, and `check` and `probe` exit non-zero when something is wrong.

```bash
npm install -g connectable.sh
# or
bun add -g connectable.sh
```

```
$ connectable check connectors.json
✓ connectors.json is valid — 2 connector(s)
  support (driver)
  guides (driver)
$ connectable probe connectors.json
✓ support — 3 tool(s), 1ms
✓ guides — 4 tool(s), 1ms
``` In a second terminal:

```bash
bun client.ts                                  # default http://localhost:3333/mcp
```

## Bring the servers you already have

```bash
connectable import-mcp claude-desktop
```

## Guides

- [Five-minute local setup](https://connectable.sh/guide#1-quick-start-self-hosted-file-mode): A folder of notes, a Postgres warehouse and GitHub behind one gateway.
- [Query your database safely](https://connectable.sh/guide#let-an-agent-query-your-database-safely): Scope a Postgres connector to the tables that matter and say what they mean.
- [Your laptop, from a hosted gateway](https://connectable.sh/guide#4-links-your-laptop-reachable-from-a-hosted-gateway): Pair a device, serve local files and SQLite, and scope them to an endpoint.

## Questions

### What is an MCP gateway?

One MCP server that sits between your agents and every tool they use. A client connects to it once, and it forwards each call to the right upstream with policy, limits and approvals applied on the way.

### Can I use the MCP servers I already have?

`connectable import-mcp claude-desktop` reads the servers you already configured and writes them into `connectors.json`, with credentials moved into `.env.connectable`. `cursor`, `windsurf`, `vscode`, `codex` and `claude-code` work the same way.

### Does the agent ever see my API keys?

No. Connectors reference `{{ secrets.NAME }}`, the value is resolved just before the upstream is dialed, and a value an upstream echoes back reaches the model as `[secret:NAME]`.

### Can an agent write to my database?

Only with `allowWrites`. Without it, every SQL driver runs one read-only statement per call and the `execute` tool does not exist.

### Do I have to self-host it?

No. Use the hosted dashboard at connectable.sh, or run the self-host binary with connectors in a JSON file. `connectable data-plane` does both: the gateway on your machine, the dashboard hosted.

### Which clients work?

Any MCP client that speaks HTTP. The dashboard prints a setup snippet for Claude Code, Codex, Cursor, Claude Desktop, Windsurf and opencode.

### What does it cost?

The hosted Free plan is $0 for one seat, five connectors and two endpoints. Team is $50 a month with five seats, then $10 a seat. Calls are never charged for.

## More

- [Reference](https://connectable.sh/reference): the README in full
- [Guide](https://connectable.sh/guide): USAGE.md in full
- [llms.txt](https://connectable.sh/llms.txt)
- [AGENTS.md](https://connectable.sh/AGENTS.md)
