sync: update MCP client from GameCraftTable

This commit is contained in:
2026-09-17 00:40:50 +08:00
parent d8f8f394f9
commit 6182738980
18 changed files with 551 additions and 23 deletions
+7 -1
View File
@@ -1,3 +1,9 @@
/**
* 覆盖 Agent 配置适配器、Skill 目录解析、幂等配置、冲突替换与备份行为。
*
* @packageDocumentation
*/
import assert from "node:assert/strict";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import os from "node:os";
@@ -35,7 +41,7 @@ class FakeRunner implements CommandRunner {
}
const options = resolveClientOptions({ url: "https://example.test/mcp", clientId: "client", callbackPort: 48321, env: {} });
const cliEntry = path.resolve("dist", "cli.js");
const cliEntry = path.resolve("..", "build", "mcp-client", "cli.js");
const nodePath = path.resolve("bin", "node.exe");
test("Agent Skill paths use each client's user-level discovery directory", () => {
+6
View File
@@ -1,3 +1,9 @@
/**
* 覆盖 stdio 代理对 MCP 工具、资源、模板、游标与上游错误的转发行为。
*
* @packageDocumentation
*/
import assert from "node:assert/strict";
import test from "node:test";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
+7 -1
View File
@@ -1,3 +1,9 @@
/**
* 覆盖连接配置校验、OAuth state/回调、元数据检查与令牌撤销边界。
*
* @packageDocumentation
*/
import assert from "node:assert/strict";
import { createServer } from "node:net";
import test from "node:test";
@@ -12,7 +18,7 @@ class MemoryTokens implements TokenStore {
readonly values = new Map<string, OAuthTokens>();
async get(account: string): Promise<OAuthTokens | undefined> { return this.values.get(account); }
async set(account: string, tokens: OAuthTokens): Promise<void> { this.values.set(account, tokens); }
async delete(account: string): Promise<void> { this.values.delete(account); }
async delete(account: string): Promise<boolean> { return this.values.delete(account); }
}
class MemoryDiscovery {
+54
View File
@@ -1,7 +1,15 @@
/**
* 覆盖系统凭据存储适配器的令牌分块、轮换清理与旧格式兼容行为。
*
* @packageDocumentation
*/
import assert from "node:assert/strict";
import test from "node:test";
import type { OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
import { KeyringTokenStore } from "../src/credentials.js";
import { credentialAccount, resolveClientOptions } from "../src/config.js";
import { logoutRemote } from "../src/remote.js";
class MemoryEntry {
constructor(private readonly values: Map<string, string>, private readonly account: string) {}
@@ -64,3 +72,49 @@ test("keyring token store still reads and removes legacy single-entry credential
await store.delete("account");
assert.equal(values.size, 0);
});
for (const damage of ["invalid-json", "missing-token-fields", "missing-chunk"] as const) {
test(`local-only logout removes ${damage} credentials without remote requests`, async () => {
const options = resolveClientOptions({ url: "https://example.test/mcp", env: {} });
const account = credentialAccount(options);
const values = new Map<string, string>();
const store = memoryStore(values);
if (damage === "missing-chunk") {
await store.set(account, { access_token: "a".repeat(3000), token_type: "Bearer" });
values.delete([...values.keys()].find((key) => key !== account)!);
} else {
values.set(account, damage === "invalid-json" ? "{broken" : "{}");
}
values.set("other-account", "untouched");
await assert.rejects(store.get(account), /credential is invalid/);
const discovery = new Map([[account, "cached"], ["other-account", "untouched"]]);
const result = await logoutRemote(options, store, {
async delete(key: string) { discovery.delete(key); },
} as never, true, async () => { throw new Error("unexpected network request"); });
assert.deepEqual(result, { hadCredential: true, revoked: false });
assert.deepEqual([...values], [["other-account", "untouched"]]);
assert.deepEqual([...discovery], [["other-account", "untouched"]]);
assert.equal(await store.get(account), undefined);
assert.deepEqual(await logoutRemote(options, store, {
async delete(key: string) { discovery.delete(key); },
} as never, true), { hadCredential: false, revoked: false });
});
}
test("local-only logout reports credential store failures without claiming success", async () => {
const store = new KeyringTokenStore(async () => { throw new Error("access denied"); });
await assert.rejects(logoutRemote(resolveClientOptions({ env: {} }), store, {
async delete() { assert.fail("discovery must be retained when credential deletion fails"); },
} as never, true), /credential store is unavailable.*access denied/);
});
test("keyring null for an absent Windows credential is treated as missing", async () => {
const store = new KeyringTokenStore(async () => ({
async getPassword() { return null; },
async setPassword() { assert.fail("unexpected credential write"); },
async deleteCredential() { return false; },
}));
assert.equal(await store.get("account"), undefined);
assert.equal(await store.delete("account"), false);
});
+7 -1
View File
@@ -1,3 +1,9 @@
/**
* 通过本地 OAuth 与 MCP 模拟服务验证完整 PKCE 登录、令牌刷新和秘密隔离流程。
*
* @packageDocumentation
*/
import assert from "node:assert/strict";
import { createHash, randomUUID } from "node:crypto";
import express from "express";
@@ -15,7 +21,7 @@ class MemoryTokens implements TokenStore {
readonly values = new Map<string, OAuthTokens>();
async get(account: string): Promise<OAuthTokens | undefined> { return this.values.get(account); }
async set(account: string, tokens: OAuthTokens): Promise<void> { this.values.set(account, tokens); }
async delete(account: string): Promise<void> { this.values.delete(account); }
async delete(account: string): Promise<boolean> { return this.values.delete(account); }
}
class MemoryDiscovery {
+6
View File
@@ -1,3 +1,9 @@
/**
* 为 stdio 协议集成测试提供最小 MCP 服务进程。
*
* @packageDocumentation
*/
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createProxyServer } from "../src/bridge.js";
+6
View File
@@ -1,3 +1,9 @@
/**
* 验证 stdio 模式的标准输出只包含合法 JSON-RPC 消息,不混入诊断文本。
*
* @packageDocumentation
*/
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import path from "node:path";