From ba7621c4cb1e792d01458b7c95089aa57c002280 Mon Sep 17 00:00:00 2001 From: chaewon-huh Date: Sat, 4 Jul 2026 11:48:33 +0900 Subject: [PATCH] feat: report hosted remote MCP setup --- README.md | 35 +++++++-- src/commands/mcp.ts | 37 +++++++++ src/lib/mcp-launch-status.ts | 18 +++++ src/lib/remote-mcp-setup.ts | 148 +++++++++++++++++++++++++++++++++++ test/cli.test.ts | 70 +++++++++++++++++ test/mcp.test.ts | 49 ++++++++++++ 6 files changed, 352 insertions(+), 5 deletions(-) create mode 100644 src/lib/remote-mcp-setup.ts diff --git a/README.md b/README.md index bad8a56..86b556e 100644 --- a/README.md +++ b/README.md @@ -291,13 +291,38 @@ billing-write, and file workflows. ## MCP -Sume MCP is coming soon and is not part of this public CLI launch release yet. -Use direct CLI commands today for auth, schema discovery, Avatar, Avatar Video, -jobs, balance, and usage workflows. +Hosted remote Sume MCP is available at: + +```text +https://mcp.sume.com/mcp +``` + +Paste that URL into a hosted remote MCP client that supports OAuth. The client +starts the Sume OAuth flow, the user signs in with Sume, and the client stores +an OAuth token for read-only MCP tools. This hosted flow is separate from local +`sume login`; it does not require users to create or paste Sume API keys. + +Show client setup guidance from the CLI: + +```bash +sume mcp remote --json +sume mcp remote --agent cursor --json +``` + +OAuth Phase 1 is read-only. Write tools and paid generation tools remain blocked +for hosted OAuth tokens. Read-only OAuth asset helpers include `assets.list`, +`assets.get`, and `assets.download_url`. The remote MCP server also exposes +write-gated `assets.upload_url` and `assets.complete` for compatible non-OAuth +sessions. `assets.upload_file` remains local-only because a hosted MCP server +cannot read the caller's local filesystem. + +Local stdio MCP is coming soon and is not part of this public CLI launch release +yet. Use direct CLI commands today for auth, schema discovery, Avatar, Avatar +Video, jobs, balance, and usage workflows. The open-source repository may contain MCP implementation code while the -capability is being prepared. Public MCP setup and server startup are not -launched yet, and the CLI does not write MCP client config in this release. +capability is being prepared. Local MCP setup and server startup are not +launched yet, and the CLI does not write local MCP client config in this release. ## Website diff --git a/src/commands/mcp.ts b/src/commands/mcp.ts index c9d63d2..7f87fe1 100644 --- a/src/commands/mcp.ts +++ b/src/commands/mcp.ts @@ -6,6 +6,7 @@ import { } from "../lib/mcp-launch-status.js"; import { supportedMcpClientAgents } from "../lib/mcp-client-config.js"; import { renderResult } from "../lib/render.js"; +import { buildRemoteMcpSetup } from "../lib/remote-mcp-setup.js"; type McpInstallOptions = { agent?: string; @@ -16,6 +17,10 @@ type McpDoctorOptions = { agent?: string; }; +type McpRemoteOptions = { + agent?: string; +}; + export function registerMcpCommand(program: Command) { const mcp = program .command("mcp", { hidden: true }) @@ -59,8 +64,40 @@ export function registerMcpCommand(program: Command) { ["Launched", payload.launched], "", payload.message, + "", + ["Hosted remote MCP", payload.remote.status], + ["Remote endpoint", payload.remote.endpoint], + ["Remote OAuth", payload.remote.oauth], "Use direct Sume CLI commands today.", ], }); }); + + mcp + .command("remote") + .description("Show hosted remote MCP OAuth setup guidance.") + .option( + "--agent ", + `Target MCP client. Supported: ${supportedMcpClientAgents().join(", ")}.`, + ) + .action((options: McpRemoteOptions, command: Command) => { + const payload = buildRemoteMcpSetup(options.agent); + renderResult(payload, { + json: getMode(command).json, + human: [ + "Sume hosted remote MCP", + "", + ["Endpoint", payload.endpoint], + ["OAuth resource", payload.oauth.resource], + ["Scopes", payload.oauth.scopes.join(", ")], + ["Local CLI login required", payload.oauth.local_cli_login_required], + ["Manual API key required", payload.oauth.manual_api_key_required], + ["OAuth write tools", payload.phase.write_tools_enabled_for_oauth], + ["OAuth paid tools", payload.phase.paid_tools_enabled_for_oauth], + "", + "Paste the endpoint into a hosted remote MCP client and complete its OAuth flow.", + "Local `sume login` remains for the CLI and does not configure hosted MCP OAuth.", + ], + }); + }); } diff --git a/src/lib/mcp-launch-status.ts b/src/lib/mcp-launch-status.ts index ad42f06..539ddae 100644 --- a/src/lib/mcp-launch-status.ts +++ b/src/lib/mcp-launch-status.ts @@ -1,4 +1,9 @@ import { CliError } from "./errors.js"; +import { + REMOTE_MCP_ENDPOINT, + REMOTE_MCP_OAUTH_RESOURCE, + REMOTE_MCP_SCOPE, +} from "./remote-mcp-setup.js"; export const MCP_COMING_SOON_MESSAGE = "Sume MCP is coming soon and is not launched in this CLI release yet."; @@ -20,6 +25,19 @@ export function mcpComingSoonStatus() { launched: false, message: MCP_COMING_SOON_MESSAGE, recommended_surface: "direct_cli", + remote: { + status: "available", + endpoint: REMOTE_MCP_ENDPOINT, + oauth: "hosted_client_flow", + resource: REMOTE_MCP_OAUTH_RESOURCE, + scopes: [REMOTE_MCP_SCOPE], + local_cli_login_required: false, + }, + local_stdio: { + status: "coming_soon", + launched: false, + command: "sume mcp", + }, next_steps: MCP_COMING_SOON_NEXT_STEPS, }; } diff --git a/src/lib/remote-mcp-setup.ts b/src/lib/remote-mcp-setup.ts new file mode 100644 index 0000000..4d6f02b --- /dev/null +++ b/src/lib/remote-mcp-setup.ts @@ -0,0 +1,148 @@ +import { CliError } from "./errors.js"; +import { + type McpClientAgent, + supportedMcpClientAgents, +} from "./mcp-client-config.js"; + +export const REMOTE_MCP_ENDPOINT = "https://mcp.sume.com/mcp"; +export const REMOTE_MCP_OAUTH_RESOURCE = REMOTE_MCP_ENDPOINT; +export const REMOTE_MCP_SCOPE = "mcp:read"; + +const REMOTE_MCP_OAUTH_ASSET_HELPERS = [ + "assets.list", + "assets.get", + "assets.download_url", +] as const; + +const REMOTE_MCP_WRITE_GATED_ASSET_HELPERS = [ + "assets.list", + "assets.get", + "assets.upload_url", + "assets.complete", + "assets.download_url", +] as const; + +const LOCAL_ONLY_MCP_ASSET_HELPERS = ["assets.upload_file"] as const; + +type RemoteMcpClientSetup = { + agent: McpClientAgent; + client: string; + setup: string[]; +}; + +const REMOTE_MCP_CLIENTS: RemoteMcpClientSetup[] = [ + { + agent: "codex", + client: "Codex", + setup: [ + "Add a streamable HTTP MCP server named `sume` with url `https://mcp.sume.com/mcp`.", + "Run the client's MCP OAuth login flow for the `sume` server.", + ], + }, + { + agent: "claude-code", + client: "Claude Code", + setup: [ + "Add a remote HTTP MCP server named `sume` with url `https://mcp.sume.com/mcp`.", + "Run the client's MCP OAuth login flow for the `sume` server.", + ], + }, + { + agent: "cursor", + client: "Cursor", + setup: [ + "Add a remote MCP server named `sume` with url `https://mcp.sume.com/mcp`.", + "Use Cursor's OAuth flow when it prompts you to connect Sume.", + ], + }, +]; + +export type RemoteMcpSetup = { + object: "remote_mcp_setup"; + schema_version: 1; + endpoint: typeof REMOTE_MCP_ENDPOINT; + oauth: { + type: "oauth"; + resource: typeof REMOTE_MCP_OAUTH_RESOURCE; + scopes: [typeof REMOTE_MCP_SCOPE]; + manual_api_key_required: false; + local_cli_login_required: false; + }; + local_stdio_mcp: { + launched: false; + command: "sume mcp"; + status: "coming_soon"; + }; + phase: { + read_only_tools_enabled: true; + write_tools_enabled_for_oauth: false; + paid_tools_enabled_for_oauth: false; + }; + asset_helpers: { + oauth_read_only_helpers: typeof REMOTE_MCP_OAUTH_ASSET_HELPERS; + remote_write_gated_helpers: typeof REMOTE_MCP_WRITE_GATED_ASSET_HELPERS; + local_only_helpers: typeof LOCAL_ONLY_MCP_ASSET_HELPERS; + }; + clients: RemoteMcpClientSetup[]; + notes: string[]; +}; + +export function buildRemoteMcpSetup(agent?: string): RemoteMcpSetup { + const clients = agent + ? [findRemoteMcpClient(agent)] + : REMOTE_MCP_CLIENTS.map((client) => ({ ...client, setup: [...client.setup] })); + + return { + object: "remote_mcp_setup", + schema_version: 1, + endpoint: REMOTE_MCP_ENDPOINT, + oauth: { + type: "oauth", + resource: REMOTE_MCP_OAUTH_RESOURCE, + scopes: [REMOTE_MCP_SCOPE], + manual_api_key_required: false, + local_cli_login_required: false, + }, + local_stdio_mcp: { + launched: false, + command: "sume mcp", + status: "coming_soon", + }, + phase: { + read_only_tools_enabled: true, + write_tools_enabled_for_oauth: false, + paid_tools_enabled_for_oauth: false, + }, + asset_helpers: { + oauth_read_only_helpers: REMOTE_MCP_OAUTH_ASSET_HELPERS, + remote_write_gated_helpers: REMOTE_MCP_WRITE_GATED_ASSET_HELPERS, + local_only_helpers: LOCAL_ONLY_MCP_ASSET_HELPERS, + }, + clients, + notes: [ + "Hosted remote MCP OAuth is separate from local `sume login`.", + "Paste the endpoint into clients that support hosted remote MCP OAuth.", + "OAuth Phase 1 is read-only; write and paid tools stay blocked for OAuth tokens.", + "`assets.upload_file` remains local-only because a hosted MCP server cannot read local files.", + ], + }; +} + +function findRemoteMcpClient(agent: string) { + if (!supportedMcpClientAgents().includes(agent as McpClientAgent)) { + throw new CliError(`Unsupported MCP agent: ${agent}`, { + code: "invalid_argument", + hint: `Use one of: ${supportedMcpClientAgents().join(", ")}`, + }); + } + + const client = REMOTE_MCP_CLIENTS.find((entry) => entry.agent === agent); + if (!client) { + throw new CliError(`Unsupported MCP agent: ${agent}`, { + code: "invalid_argument", + hint: `Use one of: ${supportedMcpClientAgents().join(", ")}`, + }); + } + + return { ...client, setup: [...client.setup] }; +} diff --git a/test/cli.test.ts b/test/cli.test.ts index fc4cbf9..b503b54 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -493,7 +493,77 @@ describe("CLI", () => { status: "coming_soon", launched: false, recommended_surface: "direct_cli", + remote: { + status: "available", + endpoint: "https://mcp.sume.com/mcp", + oauth: "hosted_client_flow", + scopes: ["mcp:read"], + local_cli_login_required: false, + }, + local_stdio: { + status: "coming_soon", + launched: false, + command: "sume mcp", + }, + }); + expect(requests).toHaveLength(0); + }, + ); + }); + + it("reports hosted remote MCP OAuth setup without calling the API", async () => { + await withMockApi( + () => ({ data: { ok: true } }), + async (baseUrl, requests) => { + const { stdout } = await runCli( + ["--json", "mcp", "remote", "--agent", "cursor"], + baseUrl, + ); + const parsed = JSON.parse(stdout); + expect(parsed).toMatchObject({ + object: "remote_mcp_setup", + endpoint: "https://mcp.sume.com/mcp", + oauth: { + type: "oauth", + resource: "https://mcp.sume.com/mcp", + scopes: ["mcp:read"], + manual_api_key_required: false, + local_cli_login_required: false, + }, + local_stdio_mcp: { + launched: false, + command: "sume mcp", + status: "coming_soon", + }, + phase: { + read_only_tools_enabled: true, + write_tools_enabled_for_oauth: false, + paid_tools_enabled_for_oauth: false, + }, + asset_helpers: { + oauth_read_only_helpers: [ + "assets.list", + "assets.get", + "assets.download_url", + ], + remote_write_gated_helpers: [ + "assets.list", + "assets.get", + "assets.upload_url", + "assets.complete", + "assets.download_url", + ], + local_only_helpers: ["assets.upload_file"], + }, + clients: [ + expect.objectContaining({ + agent: "cursor", + client: "Cursor", + }), + ], }); + expect(JSON.stringify(parsed)).not.toMatch(/sume_(?:live|test)_/u); + expect(JSON.stringify(parsed)).not.toMatch(/bearer\s+[A-Za-z0-9._-]+/iu); expect(requests).toHaveLength(0); }, ); diff --git a/test/mcp.test.ts b/test/mcp.test.ts index ae6cb21..09cd874 100644 --- a/test/mcp.test.ts +++ b/test/mcp.test.ts @@ -10,6 +10,7 @@ import { inspectMcpClientConfig, supportedMcpClientAgents, } from "../src/lib/mcp-client-config.js"; +import { buildRemoteMcpSetup } from "../src/lib/remote-mcp-setup.js"; import { listToolSchemas } from "../src/lib/tool-registry.js"; import { formatMcpToolResponse } from "../src/mcp/server.js"; import { @@ -1808,6 +1809,54 @@ describe("MCP tool registry", () => { }); describe("MCP install dry-run registry", () => { + it("describes hosted remote MCP OAuth without local CLI login or API keys", () => { + const result = buildRemoteMcpSetup("codex"); + expect(result).toMatchObject({ + object: "remote_mcp_setup", + endpoint: "https://mcp.sume.com/mcp", + oauth: { + type: "oauth", + resource: "https://mcp.sume.com/mcp", + scopes: ["mcp:read"], + manual_api_key_required: false, + local_cli_login_required: false, + }, + local_stdio_mcp: { + launched: false, + command: "sume mcp", + status: "coming_soon", + }, + phase: { + read_only_tools_enabled: true, + write_tools_enabled_for_oauth: false, + paid_tools_enabled_for_oauth: false, + }, + asset_helpers: { + oauth_read_only_helpers: [ + "assets.list", + "assets.get", + "assets.download_url", + ], + remote_write_gated_helpers: [ + "assets.list", + "assets.get", + "assets.upload_url", + "assets.complete", + "assets.download_url", + ], + local_only_helpers: ["assets.upload_file"], + }, + clients: [ + expect.objectContaining({ + agent: "codex", + client: "Codex", + }), + ], + }); + expect(JSON.stringify(result)).not.toMatch(/sume_(?:live|test)_/u); + expect(JSON.stringify(result)).not.toMatch(/bearer\s+[A-Za-z0-9._-]+/iu); + }); + it("generates read-only config snippets for supported clients", () => { expect(supportedMcpClientAgents()).toEqual(["codex", "claude-code", "cursor"]);