Skip to content
Median
Esc
↑↓navigate↵open⌘Jpreview
On this page

MCP server

Connect Claude, Cursor, VS Code, Codex or any MCP client to your workspace.

https://api.median.sh/mcp

The server has two tools. median_run runs TypeScript against a typed median client, so one call can filter, join, loop and batch. median_docs returns that client’s TypeScript declarations. Admins and owners also find the URL in Settings → API under MCP.

Connect a client

claude mcp add --transport http median https://api.median.sh/mcp

Run /mcp in Claude Code, pick median and sign in. Add --scope user to use it in every project.

Add a custom connector in Claude under Customize → Connectors, with the server URL. Then click Connect and sign in. On Team and Enterprise plans an owner adds the connector first, in Organization settings → Connectors.

{
  "mcpServers": {
    "median": { "url": "https://api.median.sh/mcp" }
  }
}

Use .cursor/mcp.json instead for one project. Cursor asks you to sign in when it connects.

{
  "servers": {
    "median": { "type": "http", "url": "https://api.median.sh/mcp" }
  }
}

VS Code asks you to sign in when the server starts.

codex mcp add median --url https://api.median.sh/mcp
codex mcp login median

Or add it to ~/.codex/config.toml:

[mcp_servers.median]
url = "https://api.median.sh/mcp"
{
  "mcpServers": {
    "median": { "serverUrl": "https://api.median.sh/mcp" }
  }
}

Any other client that speaks streamable HTTP and OAuth works with the same URL. See Protocol.

Authorize

The client opens Median in a browser. Sign in, then click Allow on the page titled Connect and the client’s name. The page names the organization the client gets and says it acts as you.

Fact Value
Organization The one open in the dashboard when you click Allow. To connect another, switch organizations in the dashboard first, or call median.account.useOrg
Acts as You. Your current role is checked on every call
New account A connection approved with no organization can still connect. In median_run, call median.account.createOrg({ name }) to make one, or median.account.useOrg({ org }) to pick one. Until then, other calls fail with no_organization
Authorization code Expires after 10 minutes

Scripts and CI

Send MEDIAN_KEY as a bearer token and skip the browser. A key acts as an admin of its organization. Inviting, changing roles and removing members refuse a key with needs_a_person.

curl https://api.median.sh/mcp \
  -H "Authorization: Bearer $MEDIAN_KEY" \
  -H "content-type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

median.account.me, createOrg and useOrg are about a person, so they need an OAuth token and refuse a key with token_required.

Tools

Tool Input Returns
median_run { code: string }, required. The body of an async function with median in scope The returned value as JSON, then a console: section with anything logged
median_docs { topic?: string }. One namespace, or omit it for the whole reference TypeScript declarations

Topics: billing, account, conversations, customers, knowledge, signals, tasks, feedback, tools, org, site, agent, integrations, webhooks, analytics, docs. An unknown topic returns an error that lists them.

A failed run comes back as a tool result with isError: true and the error message.

The median client

await freely, log with console.log, and return the value you want back. Types are stripped before the code runs, so they are not checked.

const waiting = await median.conversations.list({
  status: "open",
  needsHuman: true,
});
return waiting.map((c) => ({ id: c.threadId ?? c.id, subject: c.subject }));
Namespace Holds
median.billing Plan, credits, invoices, metered usage, API limits and the activity log
median.account Who the token is, its organizations, and the workspace’s keys
median.conversations List, read, reply, note, resolve, archive, snooze, pause the AI, delete
median.customers The directory, learned facts, profile refresh, reach out first, delete
median.knowledge Documents, folders, search, the review queue, crawls, source syncs
median.signals List, file, move, merge and accept bugs and suggestions, and their claimed commits
median.tasks Cards, columns, requests and board settings
median.feedback Hands over a raw note. Median files it as a bug or suggestion, or drops it as spam
median.tools Approvals, manual runs, tests, switches, endpoints and tool suggestions
median.org The organization, members and their roles, invitations
median.site The public site’s look, custom domain and sign-in
median.agent Name, personality and behavior switches
median.integrations Email, Slack, Discord, GitHub, Notion and Linear settings after connecting
median.webhooks List, add, update and remove endpoints
median.analytics Dashboard numbers, every chart, costs, the explorer and each dataset’s fields
median.docs Search Median’s documentation and read a page

Every method calls the same backend function as its management API route, so shapes and errors match that reference. Roles apply the same way. See roles.

The same methods run outside the sandbox too. median call <method> in the CLI and POST /v1/call/{method} take the same names and arguments, and return the same values. The Median assistant works through the same list, so anything it can do, a method here can do.

A median.sh link works in place of an id in the methods of median.conversations, median.customers, median.knowledge and median.signals, and as the signalId of median.tasks.adopt:

const link = "https://median.sh/signal/q5776nawkbmazaj4qv4p26y3v98d83qc";
const { signal } = await median.signals.get({ signalId: link });
return signal.title;

The conversationId arguments of median.tools and median.billing.logs take the bare id.

Sandbox limits

The code runs in an isolated JavaScript engine with no network, no filesystem and no timers. median calls are real reads and writes.

Limit Value
Run time 120 seconds, median calls included
Memory 256 MB
Stack 1 MB
Result 400,000 characters of JSON. A larger result is replaced by a message saying how big it was
Text sent to the client 120,000 characters, result and console together. Longer text is cut and marked as truncated
Console 200 lines, 4,000 characters each
Timers None. setTimeout is not defined, and a run left waiting on a promise that no median call will settle fails at once
median calls Each counts as one API request against your rate limits. Calls started together run in parallel
Returning nothing Gives null

Anything shaped like a Median credential is replaced with [redacted] in error text.

Protocol

Fact Value
Transport Streamable HTTP, stateless. POST /mcp only. GET and DELETE return 405
Sessions and server stream None
Batches Not accepted. Send one JSON-RPC message per request
Protocol versions 2025-06-18, 2025-03-26, 2024-11-05. Any other version is answered with 2025-06-18
CORS access-control-allow-origin: *
Credentials An OAuth access token or MEDIAN_KEY, as a bearer token
No or dead credential 401 with WWW-Authenticate: Bearer resource_metadata="https://api.median.sh/.well-known/oauth-protected-resource"
Discovery /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server, each also with /mcp appended
Client registration Dynamic, POST /oauth/register. Public clients with no secret. Names over 80 characters are cut
Redirect URIs https anywhere, http on localhost or 127.0.0.1, or a native app scheme. The first 10 per client are kept
PKCE Required, S256 only
Grants Authorization code, refresh token, device code
Scope median
Revocation POST /oauth/revoke

Sign in limits

POST /oauth/register and POST /oauth/device_authorization are limited per caller address and answer 429 with slow_down when over. See Rate limits.

Access and revocation

Fact Value
Access token 8 hours
Refresh token Replaced on every refresh. Expires 30 days after the last one
Connections list Settings → API under MCP, for admins and owners. Each row shows the client, who approved it and when it was last used
Disconnect Click Disconnect on the row. The client is signed out on its next request
Role change Applies on the next call
Leaving the organization Every median call fails with invalid_token

Troubleshooting

Message Cause Fix
This connection has no organization yet. Create or pick one first… Approved before any organization existed Call median.account.createOrg or median.account.useOrg in median_run
That token has expired. Your client should refresh and retry. The access token is over 8 hours old Most clients refresh on their own. Reconnect if yours does not
That token does not open anything. Sign in again. The connection was disconnected or revoked Connect again
That refresh token no longer works. Connect again from your MCP client. Unused for 30 days, or disconnected Connect again
<name> is no longer in <organization>, so this connection no longer works. The person who approved it left Connect as a current member
Only admins and owners can… Your role cannot call that method Ask an admin, or see roles
The result was … characters, which is too big to hand back. The returned value is over 400,000 characters Return less. Filter, pick fields or pass limits
…truncated at 120000 characters. Result and console are over 120,000 characters Return less, or log less
The run took longer than 120 seconds and was stopped. Too much work in one run Split it into several runs
The run is waiting on a promise nothing will ever settle. The code awaited a promise with no median call behind it Await only median calls

Was this page helpful?