Remote MCP (Streamable HTTP)
Picora’s hosted MCP server speaks MCP Streamable HTTP at a single endpoint, authenticated with OAuth 2.1 + PKCE. There is nothing to install and no API Key on your machine: the client discovers the authorization server, opens a browser for consent, and receives a short-lived Bearer token.
Endpoint
| MCP endpoint | POST https://mcp.picora.me/mcp (China: https://mcp.picora.cn/mcp) |
| Transport | MCP Streamable HTTP, stateless |
| Protocol versions | 2025-06-18 (latest) · 2025-03-26 · 2024-11-05 |
| Authorization | Authorization: Bearer <oauth_access_token> |
| Resource metadata | GET /.well-known/oauth-protected-resource (RFC 9728, unauthenticated) |
| Health | GET /healthz → { ok, ts, oauth_enabled } |
Verified end-to-end with the official @modelcontextprotocol/sdk 1.x Client over StreamableHTTPClientTransport.
Why choose remote over stdio
stdio (@picora/mcp-server) | Remote (mcp.picora.me) | |
|---|---|---|
| Installation | Node.js ≥ 20 + npx -y @picora/mcp-server | Nothing — paste a URL |
| Authentication | API Key (sk_live_…) | OAuth 2.1 authorization code + PKCE |
| Secret in client config | Yes — the key sits in a plaintext config file | No — only short-lived tokens the client stores itself |
| Revocation | Delete the key in the dashboard | Revoke the grant per client; refresh-token replay auto-revokes the chain |
Local file upload (filePath) | Yes — reads straight off your disk | No — the server has no filesystem; use url or base64 |
| Cross-device | Per-machine setup | Same account, any device or web client |
| Availability | Coming soon (npm package unpublished) | Coming soon (not deployed) |
Pick stdio when the assistant must upload files sitting on the local disk. Pick remote for web and mobile clients, shared or managed machines, and anywhere you don’t want a long-lived API Key written to a config file.
Transport behaviour
Stateless is a branch the MCP specification explicitly permits, and it shapes what a client sees:
- No session. The server never issues
Mcp-Session-Id; every request carries its own Bearer token. - No SSE. Responses are always
application/json— the spec lets a server choose JSON or an SSE stream, and Picora chooses JSON. GET /mcpandDELETE /mcpreturn405withAllow: POST. There is no server-initiated stream to open and no session to terminate. Official SDK clients handle this gracefully.- No JSON-RPC batching. Batching was removed in
2025-06-18; an array payload is rejected with-32600. Send one message per request. - Notifications get
202. A message without anid(e.g.notifications/initialized) returns HTTP 202 with an empty body and no JSON-RPC response.
Protocol version negotiation
- Send
MCP-Protocol-Version: 2025-06-18on every request afterinitialize. - Omit the header and the server assumes
2025-03-26— the header was introduced in2025-06-18, so its absence implies the previous revision. - Send an explicit version the server doesn’t support and you get
400with{ "error": "unsupported_protocol_version", "supported": [...] }. - In
initialize, a recognisedprotocolVersionis echoed back; anything else negotiates down to2025-06-18.
Supported methods
| Method | Behaviour |
|---|---|
initialize | Returns negotiated protocolVersion, capabilities.tools.listChanged: false, and serverInfo (picora-mcp) |
ping | Empty result |
tools/list | Single page of tool definitions with full inputSchema. Cursors are ignored — the tool set is small and fixed, so no nextCursor is returned |
tools/call | Runs the tool. See error mapping below |
Error mapping
| Situation | Response |
|---|---|
| Unknown tool name | JSON-RPC error -32602 (Invalid params) |
| Tool ran but failed (scope denied, quota, upstream error) | JSON-RPC result with isError: true and the detail in content |
| Malformed JSON | -32700 Parse error |
| Array payload / non-object payload | -32600 Invalid Request |
| Missing or invalid Bearer token | HTTP 401 with WWW-Authenticate (see below) |
| Service disabled pending audit | HTTP 503 temporarily_unavailable |
Note the split: a tool failing is a successful JSON-RPC call carrying isError: true, not a protocol error. Only a tool that doesn’t exist becomes -32602.
OAuth bootstrap
A standards-compliant MCP client configures itself from the URL alone — you never paste an endpoint list by hand.
-
Client calls
POST /mcpwithout a token. The server answers401with:WWW-Authenticate: Bearer error="invalid_token",error_description="...",resource_metadata="https://mcp.picora.me/.well-known/oauth-protected-resource" -
Client fetches the resource metadata (RFC 9728). It is public, cacheable for an hour, and served even while the master switch is off:
{"resource": "https://mcp.picora.me","authorization_servers": ["https://api.picora.me"],"bearer_methods_supported": ["header"],"scopes_supported": ["media.read", "media.write", "media.delete","kb.read", "kb.write", "usage.read", "account.read"],"resource_name": "Picora MCP"} -
Client discovers the authorization server at
GET https://api.picora.me/.well-known/oauth-authorization-server(RFC 8414), which advertises the consent, token, registration and revocation endpoints, the JWKS URI,code_challenge_methods_supported: ["S256"],grant_types_supported: ["authorization_code", "refresh_token"], andtoken_endpoint_auth_methods_supported: ["none"]— Picora treats MCP clients as public clients, so PKCE is mandatory and there is no client secret. -
Client registers itself via RFC 7591 dynamic registration at
POST /oauth/register, or uses a pre-registeredclient_id. -
Browser consent. The client opens the authorization endpoint with an S256
code_challenge; you sign in and pick which scopes to grant. -
Token exchange. The client swaps the authorization code plus
code_verifierfor an RS256-signed access token and a refresh token. -
Every MCP request carries the Bearer token. The server verifies the signature offline against the JWKS and checks the revocation list — no round-trip to the authorization server per call.
Refresh tokens rotate on use. Replaying a rotated refresh token revokes the entire token chain and alerts the account owner.
Client configuration
Exact wording varies between client versions; the endpoint never does.
Cursor
~/.cursor/mcp.json:
{ "mcpServers": { "picora": { "url": "https://mcp.picora.me/mcp" } }}Restart Cursor. The first tool call opens the OAuth consent page in your browser; approve once.
Claude Desktop
Add a custom connector in settings and paste https://mcp.picora.me/mcp. Claude Desktop performs the discovery and OAuth flow described above; approve in the browser window it opens.
Clients that only speak stdio
Bridge the remote endpoint through the community mcp-remote adapter:
{ "mcpServers": { "picora": { "command": "npx", "args": ["-y", "mcp-remote", "https://mcp.picora.me/mcp"] } }}The bridge handles the OAuth flow locally and speaks stdio to the host. It still cannot upload local filePath arguments — the remote server has no filesystem access either way.
China deployment
Replace mcp.picora.me with mcp.picora.cn; discovery points at api.picora.cn automatically. The two regions have completely separate accounts, data and tokens.
Verifying by hand
Once the endpoint is live:
# Discovery works without a tokencurl -s https://mcp.picora.me/.well-known/oauth-protected-resource | jq
# Health, including whether OAuth is switched oncurl -s https://mcp.picora.me/healthz | jq
# Unauthenticated call → 401 carrying resource_metadatacurl -si -X POST https://mcp.picora.me/mcp \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | head -20
# Authenticated tool listingcurl -s -X POST https://mcp.picora.me/mcp \ -H "Authorization: Bearer $PICORA_OAUTH_TOKEN" \ -H 'Content-Type: application/json' \ -H 'MCP-Protocol-Version: 2025-06-18' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq '.result.tools | length'Until deployment, the tool catalog is still inspectable without any of this:
curl -s https://api.picora.me/mcp/tools.json | jq '.version, .totals'Scopes to request
The resource metadata advertises seven scopes. Ask for the narrowest set that does the job:
| Goal | Scopes |
|---|---|
| Read-only research assistant | media.read, kb.read, usage.read |
| Assistant that uploads and edits | add media.write, kb.write |
| Assistant that cleans up old assets | add media.delete — deletion is irreversible |
account.read covers basic profile information and is not required by any of the 19 tools. See Concepts for how scopes map to individual tools, and the tool reference for per-tool requirements.