import assert from "node:assert/strict"; import { mkdtempSync, readFileSync, statSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; import { APPROVAL_ONBOARDING_EXAMPLES, RemoteBridge, bridgeAuthToolResult, createBridgeMcpServer } from "../src/bridge.js"; test("stdio bridge creates a messenger-friendly device login challenge and protects local state", async () => { const calls: string[] = []; const fetchFn: typeof fetch = async (input) => { const url = String(input); calls.push(url); if (url.endsWith("/register")) return Response.json({ client_id: "bridge-client" }, { status: 201 }); if (url.endsWith("/oauth/device/authorize")) return Response.json({ device_code: "device-secret", user_code: "ABCD-2345", verification_uri_complete: "https://approval.example.test/oauth/device?user_code=ABCD-2345", expires_in: 600, interval: 5, }); if (url.endsWith("/oauth/device/token")) return Response.json({ error: "authorization_pending" }, { status: 400 }); throw new Error(`unexpected ${url}`); }; const root = mkdtempSync(join(tmpdir(), "ding-bridge-")); const statePath = join(root, "private", "session.json"); const bridge = new RemoteBridge("https://approval.example.test/mcp", statePath, fetchFn); const first = await bridge.loginChallenge(); assert.equal(first.authenticated, false); assert.ok("authChallenge" in first); assert.equal(first.authChallenge.directUrl, "https://approval.example.test/oauth/device?user_code=ABCD-2345"); assert.match(first.message, /https:\/\/approval\.example\.test\/oauth\/device\?user_code=ABCD-2345/); assert.match(first.message, /\[打开钉钉授权登录\]\(https:\/\/approval\.example\.test\/oauth\/device\?user_code=ABCD-2345\)/); assert.match(first.message, /不会另行推送或弹出/); assert.match(first.message, /当前回复正文中原样显示/); assert.match(first.message, /授权码:ABCD-2345/); const toolResult = bridgeAuthToolResult(first); assert.equal(toolResult.content[0]?.type, "text"); assert.match(toolResult.content[0]!.type === "text" ? toolResult.content[0].text : "", /备用原始链接:https:\/\/approval\.example\.test/); assert.deepEqual(toolResult.content[1], { type: "resource_link", name: "dingtalk-approval-login", title: "打开钉钉授权登录", uri: "https://approval.example.test/oauth/device?user_code=ABCD-2345", description: "当前用户的短期钉钉授权登录链接", }); assert.equal(statSync(statePath).mode & 0o777, 0o600); assert.equal(statSync(join(root, "private")).mode & 0o777, 0o700); assert.match(readFileSync(statePath, "utf8"), /device-secret/); const second = await bridge.loginChallenge(); assert.equal(second.authenticated, false); assert.deepEqual(calls.map((url) => new URL(url).pathname), ["/register", "/oauth/device/authorize", "/oauth/device/token"]); }); test("post-install onboarding waits for authentication and is visible exactly once in portable text", async () => { const root = mkdtempSync(join(tmpdir(), "ding-bridge-onboarding-login-")); const statePath = join(root, "session.json"); let authorized = false; const bridge = new RemoteBridge("https://approval.example.test/mcp", statePath, async (input) => { const path = new URL(String(input)).pathname; if (path === "/register") return Response.json({ client_id: "bridge-client" }, { status: 201 }); if (path === "/oauth/device/authorize") return Response.json({ device_code: "onboarding-device", user_code: "START-1234", verification_uri_complete: "https://approval.example.test/oauth/device?user_code=START-1234", expires_in: 600, interval: 5, }); if (path === "/oauth/device/token") return authorized ? Response.json({ access_token: "fresh-access", refresh_token: "fresh-refresh", expires_in: 900 }) : Response.json({ error: "authorization_pending" }, { status: 400 }); throw new Error(`unexpected ${path}`); }); bridge.ensureOnboardingPending(); const challenge = await bridge.loginChallenge(); assert.equal(challenge.authenticated, false); assert.equal(JSON.parse(readFileSync(statePath, "utf8")).onboarding.status, "pending", "showing a login URL must not consume onboarding"); authorized = true; const authenticated = await bridge.loginChallenge(); assert.equal(authenticated.authenticated, true); assert.ok("onboarding" in authenticated && authenticated.onboarding); assert.deepEqual(authenticated.onboarding.examples, [...APPROVAL_ONBOARDING_EXAMPLES]); const portable = bridgeAuthToolResult(authenticated); const portableText = portable.content[0]?.type === "text" ? portable.content[0].text : ""; assert.match(portableText, /首次登录已完成/); assert.match(portableText, /帮我列出当前待处理的审批/); assert.equal(JSON.parse(readFileSync(statePath, "utf8")).onboarding.status, "delivered"); const repeated = await bridge.loginChallenge(); assert.equal("onboarding" in repeated, false); assert.doesNotMatch(repeated.message, /首次登录已完成/); }); test("the first successful business response can deliver onboarding without an explicit login tool call", async () => { const root = mkdtempSync(join(tmpdir(), "ding-bridge-onboarding-call-")); const statePath = join(root, "session.json"); writeFileSync(statePath, JSON.stringify({ client: { client_id: "bridge-client" }, tokens: { access_token: "fresh-access", savedAt: Date.now(), expires_in: 900 }, })); const bridge = new RemoteBridge("https://approval.example.test/mcp", statePath, async () => { throw new Error("network should not be used by the injected client"); }); bridge.ensureOnboardingPending(); (bridge as unknown as { client: { callTool(): Promise; close(): Promise } }).client = { async callTool() { return { isError: true, content: [{ type: "text", text: "upstream failed" }] }; }, async close() {}, }; const failed = await bridge.call("approval_list_todos", { source: "all", query: "", limit: 1 }); assert.equal(failed.isError, true); assert.equal(JSON.parse(readFileSync(statePath, "utf8")).onboarding.status, "pending", "an error response must not consume onboarding"); (bridge as unknown as { client: { callTool(): Promise; close(): Promise } }).client = { async callTool() { return { content: [{ type: "text", text: "已实时查询审批待办。" }], structuredContent: { items: [] } }; }, async close() {}, }; const first = await bridge.call("approval_list_todos", { source: "all", query: "", limit: 1 }); assert.match(first.content.at(-1).text, /首次登录已完成/); assert.deepEqual(first.structuredContent.onboarding.examples, [...APPROVAL_ONBOARDING_EXAMPLES]); const second = await bridge.call("approval_list_todos", { source: "all", query: "", limit: 1 }); assert.equal(second.content.length, 1); assert.equal(second.structuredContent.onboarding, undefined); }); test("stdio bridge discards a connected client after its access token expires", async () => { const root = mkdtempSync(join(tmpdir(), "ding-bridge-expired-")); const statePath = join(root, "session.json"); writeFileSync(statePath, JSON.stringify({ client: { client_id: "bridge-client" }, tokens: { access_token: "expired-access", savedAt: Date.now() - 120_000, expires_in: 60 }, })); const paths: string[] = []; const bridge = new RemoteBridge("https://approval.example.test/mcp", statePath, async (input) => { const path = new URL(String(input)).pathname; paths.push(path); if (path === "/oauth/device/authorize") return Response.json({ device_code: "replacement-device", user_code: "NEXT-1234", verification_uri_complete: "https://approval.example.test/oauth/device?user_code=NEXT-1234", expires_in: 600, interval: 5, }); throw new Error(`unexpected ${path}`); }); let staleCalls = 0; let closed = 0; (bridge as unknown as { client: { callTool(): Promise; close(): Promise } }).client = { async callTool() { staleCalls += 1; return { structuredContent: { result: { stale: true } } }; }, async close() { closed += 1; }, }; const result = await bridge.call("approval_list_todos", { source: "xft", query: "", limit: 20 }); assert.equal(staleCalls, 0); assert.equal(closed, 1); assert.deepEqual(paths, ["/oauth/device/authorize"]); assert.equal(result.structuredContent.result.authenticated, false); assert.equal(result.structuredContent.result.authChallenge.directUrl, "https://approval.example.test/oauth/device?user_code=NEXT-1234"); assert.match(result.content[0].text, /https:\/\/approval\.example\.test\/oauth\/device\?user_code=NEXT-1234/); assert.match(result.content[0].text, /授权码:NEXT-1234/); assert.equal(result.content[1].type, "resource_link"); assert.equal(result.content[1].uri, "https://approval.example.test/oauth/device?user_code=NEXT-1234"); }); test("MCP approval_login returns both portable text and a standard resource link", async () => { const root = mkdtempSync(join(tmpdir(), "ding-bridge-mcp-login-")); const bridge = new RemoteBridge("https://approval.example.test/mcp", join(root, "session.json"), async (input) => { const path = new URL(String(input)).pathname; if (path === "/register") return Response.json({ client_id: "bridge-client" }, { status: 201 }); if (path === "/oauth/device/authorize") return Response.json({ device_code: "device-secret", user_code: "LINK-2345", verification_uri_complete: "https://approval.example.test/oauth/device?user_code=LINK-2345", expires_in: 600, interval: 5, }); throw new Error(`unexpected ${path}`); }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); const server = createBridgeMcpServer(bridge); const client = new Client({ name: "bridge-login-test", version: "1.0.0" }); await server.connect(serverTransport); try { await client.connect(clientTransport); const response = await client.callTool({ name: "approval_login", arguments: {} }); const content = response.content as Array<{ type: string; text?: string; uri?: string }>; const text = content.find((item) => item.type === "text")?.text ?? ""; const link = content.find((item) => item.type === "resource_link"); assert.match(text, /\[打开钉钉授权登录\]\(https:\/\/approval\.example\.test\/oauth\/device\?user_code=LINK-2345\)/); assert.match(text, /备用原始链接:https:\/\/approval\.example\.test\/oauth\/device\?user_code=LINK-2345/); assert.equal(link?.uri, "https://approval.example.test/oauth/device?user_code=LINK-2345"); assert.equal((response.structuredContent as any).result.authChallenge.directUrl, link?.uri); const tools = await client.listTools(); const history = tools.tools.find((tool) => tool.name === "approval_list_history")!; assert.equal((history.inputSchema as any).properties.startDate.default, ""); assert.equal((history.inputSchema as any).properties.endDate.default, ""); const batch = tools.tools.find((tool) => tool.name === "approval_process_batch")!; assert.equal((batch.inputSchema as any).properties.items.maxItems, 20); assert.match(batch.description ?? "", /最多 20 条.*零写入/s); } finally { await client.close(); await server.close(); } }); test("stdio bridge refreshes an expired login before creating a new challenge", async () => { const root = mkdtempSync(join(tmpdir(), "ding-bridge-refresh-")); const statePath = join(root, "session.json"); writeFileSync(statePath, JSON.stringify({ client: { client_id: "bridge-client" }, tokens: { access_token: "expired-access", refresh_token: "refresh-1", savedAt: Date.now() - 120_000, expires_in: 60, }, })); const paths: string[] = []; const bridge = new RemoteBridge("https://approval.example.test/mcp", statePath, async (input) => { const path = new URL(String(input)).pathname; paths.push(path); if (path === "/token") return Response.json({ access_token: "fresh-access", refresh_token: "refresh-2", expires_in: 900, }); throw new Error(`unexpected ${path}`); }); const result = await bridge.loginChallenge(); assert.equal(result.authenticated, true); assert.deepEqual(paths, ["/token"]); const saved = readFileSync(statePath, "utf8"); assert.match(saved, /fresh-access/); assert.doesNotMatch(saved, /expired-access/); }); test("stdio bridge preserves refresh credentials across a transient token endpoint failure", async () => { const root = mkdtempSync(join(tmpdir(), "ding-bridge-transient-refresh-")); const statePath = join(root, "session.json"); writeFileSync(statePath, JSON.stringify({ client: { client_id: "bridge-client" }, tokens: { access_token: "expired-access", refresh_token: "keep-refresh", savedAt: Date.now() - 120_000, expires_in: 60 }, })); const bridge = new RemoteBridge("https://approval.example.test/mcp", statePath, async (input) => { assert.equal(new URL(String(input)).pathname, "/token"); return Response.json({ error: "server_error" }, { status: 503 }); }); await assert.rejects(bridge.loginChallenge(), /暂时无法刷新登录/); const saved = readFileSync(statePath, "utf8"); assert.match(saved, /keep-refresh/); assert.match(saved, /expired-access/); }); test("stdio bridge adopts a newer token saved by another bridge process", async () => { const root = mkdtempSync(join(tmpdir(), "ding-bridge-shared-")); const statePath = join(root, "session.json"); const oldSavedAt = Date.now() - 120_000; writeFileSync(statePath, JSON.stringify({ client: { client_id: "bridge-client" }, tokens: { access_token: "expired-access", refresh_token: "stale-refresh", savedAt: oldSavedAt, expires_in: 60 }, })); const bridge = new RemoteBridge("https://approval.example.test/mcp", statePath, async () => { throw new Error("newer disk token should avoid refresh"); }); writeFileSync(statePath, JSON.stringify({ client: { client_id: "bridge-client" }, tokens: { access_token: "shared-fresh-access", refresh_token: "shared-refresh", savedAt: Date.now(), expires_in: 900 }, })); const result = await bridge.loginChallenge(); assert.equal(result.authenticated, true); }); test("stdio bridge bounds concurrent remote tool calls", async () => { const root = mkdtempSync(join(tmpdir(), "ding-bridge-concurrency-")); const statePath = join(root, "session.json"); writeFileSync(statePath, JSON.stringify({ client: { client_id: "bridge-client" }, tokens: { access_token: "fresh-access", savedAt: Date.now(), expires_in: 900 }, })); const bridge = new RemoteBridge("https://approval.example.test/mcp", statePath, async () => { throw new Error("network should not be used by the injected client"); }); let active = 0; let maximum = 0; (bridge as unknown as { client: { callTool(): Promise; close(): Promise } }).client = { async callTool() { active += 1; maximum = Math.max(maximum, active); await new Promise((resolve) => setTimeout(resolve, 5)); active -= 1; return { content: [{ type: "text", text: "ok" }] }; }, async close() {}, }; await Promise.all(Array.from({ length: 7 }, () => bridge.call("approval_get_actions", { approvalId: "opaque" }))); assert.equal(maximum, 3); });