Median

MCP server

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

Updated Oct 4, 20266 minute read
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 Code

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.

Claude Desktop

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.

Cursor

~/.cursor/mcp.json
{
  "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.

VS Code

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

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

Codex

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"

Windsurf

mcp_config.json
{
  "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.

FactValue
OrganizationThe one open in the dashboard when you click Allow. To connect another, switch organizations in the dashboard first, or call median.account.useOrg
Acts asYou. Your current role is checked on every call
New accountA 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 codeExpires 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

ToolInputReturns
median_run{ code: string }, required. The body of an async function with median in scopeThe returned value as JSON, then a console: section with anything logged
median_docs{ topic?: string }. One namespace, or omit it for the whole referenceTypeScript 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 }));
NamespaceHolds
median.billingPlan, credits, invoices, metered usage, API limits and the activity log
median.accountWho the token is, its organizations, and the workspace's keys
median.conversationsList, read, reply, note, resolve, archive, snooze, pause the AI, delete
median.customersThe directory, learned facts, profile refresh, reach out first, delete
median.knowledgeDocuments, folders, search, the review queue, crawls, source syncs
median.signalsList, file, move, merge and accept bugs and suggestions, and their claimed commits
median.tasksCards, columns, requests and board settings
median.feedbackHands over a raw note. Median files it as a bug or suggestion, or drops it as spam
median.toolsApprovals, manual runs, tests, switches, endpoints and tool suggestions
median.orgThe organization, members and their roles, invitations
median.siteThe public site's look, custom domain and sign-in
median.agentName, personality and behavior switches
median.integrationsEmail, Slack, Discord, GitHub, Notion and Linear settings after connecting
median.webhooksList, add, update and remove endpoints
median.analyticsDashboard numbers, every chart, costs, the explorer and each dataset's fields
median.docsSearch 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.

LimitValue
Run time120 seconds, median calls included
Memory256 MB
Stack1 MB
Result400,000 characters of JSON. A larger result is replaced by a message saying how big it was
Text sent to the client120,000 characters, result and console together. Longer text is cut and marked as truncated
Console200 lines, 4,000 characters each
TimersNone. setTimeout is not defined, and a run left waiting on a promise that no median call will settle fails at once
median callsEach counts as one API request against your rate limits. Calls started together run in parallel
Returning nothingGives null

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

Protocol

FactValue
TransportStreamable HTTP, stateless. POST /mcp only. GET and DELETE return 405
Sessions and server streamNone
BatchesNot accepted. Send one JSON-RPC message per request
Protocol versions2025-06-18, 2025-03-26, 2024-11-05. Any other version is answered with 2025-06-18
CORSaccess-control-allow-origin: *
CredentialsAn OAuth access token or MEDIAN_KEY, as a bearer token
No or dead credential401 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 registrationDynamic, POST /oauth/register. Public clients with no secret. Names over 80 characters are cut
Redirect URIshttps anywhere, http on localhost or 127.0.0.1, or a native app scheme. The first 10 per client are kept
PKCERequired, S256 only
GrantsAuthorization code, refresh token, device code
Scopemedian
RevocationPOST /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

FactValue
Access token8 hours
Refresh tokenReplaced on every refresh. Expires 30 days after the last one
Connections listSettings → API under MCP, for admins and owners. Each row shows the client, who approved it and when it was last used
DisconnectClick Disconnect on the row. The client is signed out on its next request
Role changeApplies on the next call
Leaving the organizationEvery median call fails with invalid_token

Troubleshooting

MessageCauseFix
This connection has no organization yet. Create or pick one first...Approved before any organization existedCall 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 oldMost clients refresh on their own. Reconnect if yours does not
That token does not open anything. Sign in again.The connection was disconnected or revokedConnect again
That refresh token no longer works. Connect again from your MCP client.Unused for 30 days, or disconnectedConnect again
<name> is no longer in <organization>, so this connection no longer works.The person who approved it leftConnect as a current member
Only admins and owners can...Your role cannot call that methodAsk an admin, or see roles
The result was ... characters, which is too big to hand back.The returned value is over 400,000 charactersReturn less. Filter, pick fields or pass limits
...truncated at 120000 characters.Result and console are over 120,000 charactersReturn less, or log less
The run took longer than 120 seconds and was stopped.Too much work in one runSplit 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 itAwait only median calls

Still need help?

    Esc