Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion content/docs/manifest.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion content/docs/navigation.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"sdks",
"cli",
"claude",
"mcp",
"mcp-server",
"public-website-api"
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,6 @@ envelopes differ. HTTP authentication errors can use `error` or `message`, while
handled preflight errors use `status: "error"` and `error`. MCP failures are tool
errors or SDK input-validation errors. None confirms registration or activation.

See [authentication](/authentication) and [MCP connection guidance](/mcp).
See [authentication](/authentication) and [MCP connection guidance](/mcp-server).
Application persistence, origin proof/reverification, provider configuration,
fan bootstrap, suspension and quota policy require their own completed contracts.
2 changes: 1 addition & 1 deletion content/docs/source/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"sdks",
"cli",
"claude",
"mcp",
"mcp-server",
"public-website-api"
]
},
Expand Down
4 changes: 2 additions & 2 deletions content/docs/source/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ and its [OpenAPI specification](https://recoupable.dev/openapi.json).
| Make your first API request | [Quickstart](/quickstart) |
| Build with Go or Python | [SDKs](/sdks) |
| Work from a terminal | [CLI](/cli) |
| Connect an AI assistant | [MCP](/mcp) and [Connect an agent](/mcp-oauth) |
| Connect an AI assistant | [MCP server](/mcp-server) and [Connect an agent](/mcp-oauth) |
| Search or read the public website without an account | [Website API](/public-website-api) |
| Find an endpoint and its request format | [API overview](/api-reference) |

Expand Down Expand Up @@ -87,7 +87,7 @@ Keep your API key secure. Do not share it publicly or commit it to version contr
<Card
title="MCP"
icon="plug"
href="/mcp"
href="/mcp-server"
>
Connect Recoup to AI assistants via the Model Context Protocol.
</Card>
Expand Down
12 changes: 6 additions & 6 deletions content/docs/source/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ and its [OpenAPI specification](https://recoupable.dev/openapi.json).
| Make your first API request | [Quickstart](https://docs.recoupable.dev/quickstart) |
| Build with Go or Python | [SDKs](https://docs.recoupable.dev/sdks) |
| Work from a terminal | [CLI](https://docs.recoupable.dev/cli) |
| Connect an AI assistant | [MCP](https://docs.recoupable.dev/mcp) and [Connect an agent](https://docs.recoupable.dev/mcp-oauth) |
| Connect an AI assistant | [MCP server](https://docs.recoupable.dev/mcp-server) and [Connect an agent](https://docs.recoupable.dev/mcp-oauth) |
| Search or read the public website without an account | [Website API](https://docs.recoupable.dev/public-website-api) |
| Find an endpoint and its request format | [API overview](https://docs.recoupable.dev/api-reference) |

Expand Down Expand Up @@ -72,7 +72,7 @@ Install and use the Recoup CLI to interact with the platform from your terminal.

Connect Claude to Recoup through MCP OAuth, then add optional workflow skills.

[MCP](https://docs.recoupable.dev/mcp)
[MCP](https://docs.recoupable.dev/mcp-server)

Connect Recoup to AI assistants via the Model Context Protocol.

Expand Down Expand Up @@ -986,9 +986,9 @@ MCP OAuth credentials stay in Claude. A REST-only script needs a separately conf
Start a fresh conversation and verify tool discovery and artist access again. Browser sign-in, plugin installation and token refresh are separate checks; none alone proves the complete workflow.


# MCP
# MCP server

Source: https://docs.recoupable.dev/mcp
Source: https://docs.recoupable.dev/mcp-server

Connect an agent to Recoup through OAuth and discover its authorized tools.

Expand Down Expand Up @@ -1303,7 +1303,7 @@ Open [Connected agents](https://app.recoupable.dev/oauth/connections) and discon
| `503` | Retry later; do not weaken authentication or callback checks. |
| Write timed out | Read back the target before retrying. Artist creation is not guaranteed idempotent; an automatic retry may create a duplicate. |

For clients without OAuth, [API-key authentication](https://docs.recoupable.dev/mcp) remains available. Keep keys out of model prompts and shared configuration. A stdio-only client needs a separately maintained adapter; the remote URL is not itself a stdio command.
For clients without OAuth, [API-key authentication](https://docs.recoupable.dev/mcp-server) remains available. Keep keys out of model prompts and shared configuration. A stdio-only client needs a separately maintained adapter; the remote URL is not itself a stdio command.

For help, contact [agent@recoupable.dev](mailto:agent@recoupable.dev). Include the client name, time, and error text; never include tokens, passwords, or API keys.

Expand Down Expand Up @@ -79054,7 +79054,7 @@ envelopes differ. HTTP authentication errors can use `error` or `message`, while
handled preflight errors use `status: "error"` and `error`. MCP failures are tool
errors or SDK input-validation errors. None confirms registration or activation.

See [authentication](https://docs.recoupable.dev/authentication) and [MCP connection guidance](https://docs.recoupable.dev/mcp).
See [authentication](https://docs.recoupable.dev/authentication) and [MCP connection guidance](https://docs.recoupable.dev/mcp-server).
Application persistence, origin proof/reverification, provider configuration,
fan bootstrap, suspension and quota policy require their own completed contracts.

Expand Down
2 changes: 1 addition & 1 deletion content/docs/source/mcp-oauth.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ Open [Connected agents](https://app.recoupable.dev/oauth/connections) and discon
| `503` | Retry later; do not weaken authentication or callback checks. |
| Write timed out | Read back the target before retrying. Artist creation is not guaranteed idempotent; an automatic retry may create a duplicate. |

For clients without OAuth, [API-key authentication](/mcp) remains available. Keep keys out of model prompts and shared configuration. A stdio-only client needs a separately maintained adapter; the remote URL is not itself a stdio command.
For clients without OAuth, [API-key authentication](/mcp-server) remains available. Keep keys out of model prompts and shared configuration. A stdio-only client needs a separately maintained adapter; the remote URL is not itself a stdio command.

For help, contact [agent@recoupable.dev](mailto:agent@recoupable.dev). Include the client name, time, and error text; never include tokens, passwords, or API keys.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: "MCP"
title: "MCP server"
description: "Connect an agent to Recoup through OAuth and discover its authorized tools."
---

Expand Down
25 changes: 25 additions & 0 deletions lib/mcp/__tests__/isBrowserPageRequest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, expect, it } from "vitest";
import { isBrowserPageRequest } from "@/lib/mcp/isBrowserPageRequest";

const request = (method: string, accept?: string) =>
new Request("https://docs.recoupable.dev/mcp", { method, headers: accept ? { accept } : {} });

describe("isBrowserPageRequest", () => {
it.each([
["a browser", "GET", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"],
["a crawler", "HEAD", "text/html"],
["curl", "GET", "*/*"],
["a markdown reader", "GET", "text/markdown"],
["a request without Accept", "GET", undefined],
])("sends %s to the guide", (_, method, accept) => {
expect(isBrowserPageRequest(request(method, accept))).toBe(true);
});

it.each([
["an MCP stream", "GET", "text/event-stream"],
["an MCP message", "POST", "application/json, text/event-stream"],
["a session close", "DELETE", "*/*"],
])("leaves %s to the MCP endpoint", (_, method, accept) => {
expect(isBrowserPageRequest(request(method, accept))).toBe(false);
});
});
6 changes: 6 additions & 0 deletions lib/mcp/handleDocsMcp.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createMcpHandler } from "@modelcontextprotocol/server";
import { siteConfig } from "../config.ts";
import { createDocsMcp } from "./createDocsMcp.ts";
import { isBrowserPageRequest } from "./isBrowserPageRequest.ts";

// Each HTTP exchange receives its own server; no session depends on a warm function.
const handler = createMcpHandler(createDocsMcp, { maxRequestBodySize: 16_384 });
Expand Down Expand Up @@ -36,6 +37,11 @@ export async function handleDocsMcp(request: Request) {
return Response.json({ error: "Unrecognized MCP host." }, { status: 403, headers: cors });
}
if (request.method === "OPTIONS") return new Response(null, { status: 204, headers: cors });
if (isBrowserPageRequest(request)) {
// People following an old guide link land on the guide, not on a JSON-RPC error.
const location = new URL("/mcp-server", request.url).toString();
return new Response(null, { status: 308, headers: { ...cors, Location: location, Vary: "Accept" } });
}
const response = await handler.fetch(request);
for (const [name, value] of Object.entries(cors)) response.headers.set(name, value);
return response;
Expand Down
12 changes: 12 additions & 0 deletions lib/mcp/isBrowserPageRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Whether a request to /mcp wants the human guide rather than the docs MCP endpoint.
* /mcp is the endpoint; the guide moved to /mcp-server, but older links (search results,
* recoupable.dev/docs/mcp) still arrive here. MCP clients always POST, or GET with
* `Accept: text/event-stream` (required by the Streamable HTTP transport), so any other
* GET is a browser, crawler, curl or markdown reader; /mcp-server negotiates markdown itself.
*/
export function isBrowserPageRequest(request: Request): boolean {
if (request.method !== "GET" && request.method !== "HEAD") return false;
const accept = request.headers.get("accept")?.toLowerCase() ?? "";
return !accept.includes("text/event-stream");
}
17 changes: 17 additions & 0 deletions tests/docs-mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,23 @@ test("a real MCP client lists both read-only tools, searches and reads a page",
}
});

test("a browser visiting /mcp is sent to the MCP server guide; MCP clients are not", async () => {
const browser = await handleDocsMcp(new Request("http://localhost:3000/mcp", {
headers: { accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "sec-fetch-dest": "document" },
}));
expect(browser.status).toBe(308);
expect(browser.headers.get("location")).toBe("http://localhost:3000/mcp-server");

const stream = await handleDocsMcp(new Request("http://localhost:3000/mcp", { headers: { accept: "text/event-stream" } }));
expect(stream.status).not.toBe(308);
const post = await handleDocsMcp(new Request("http://localhost:3000/mcp", {
method: "POST",
headers: { accept: "application/json, text/event-stream", "content-type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "ping" }),
}));
expect(post.status).not.toBe(308);
});

test("an untrusted host is refused and preflight is answered", async () => {
const refused = await handleDocsMcp(new Request("https://evil.example/mcp", { method: "POST" }));
expect(refused.status).toBe(403);
Expand Down
Loading