sync: update MCP client from GameCraftTable
This commit is contained in:
Vendored
+93
-4
@@ -64,14 +64,25 @@ var DiscoveryStore = class {
|
||||
constructor(configDir = platformConfigDir()) {
|
||||
this.filePath = path.join(configDir, "discovery.json");
|
||||
}
|
||||
/**
|
||||
* @param account - 稳定账户标识。
|
||||
* @returns 账户的缓存发现状态;不存在时返回 `undefined`。
|
||||
*/
|
||||
async get(account) {
|
||||
return (await this.read()).entries[account];
|
||||
}
|
||||
/**
|
||||
* 使用原子文件替换持久化账户的发现状态。
|
||||
*
|
||||
* @param account - 稳定账户标识。
|
||||
* @param state - MCP OAuth 客户端提供的发现状态。
|
||||
*/
|
||||
async set(account, state) {
|
||||
const document = await this.read();
|
||||
document.entries[account] = state;
|
||||
await atomicWriteJson(this.filePath, document);
|
||||
}
|
||||
/** @param account - 要删除其状态的稳定账户标识。 */
|
||||
async delete(account) {
|
||||
const document = await this.read();
|
||||
if (!(account in document.entries)) return;
|
||||
@@ -392,6 +403,12 @@ function safeCommandError(result) {
|
||||
return (result.stderr || result.stdout || `exit code ${result.code}`).trim();
|
||||
}
|
||||
var SpawnCommandRunner = class {
|
||||
/**
|
||||
* @param command - 可执行文件名称或路径。
|
||||
* @param args - 不经 shell 解释而传递的原始参数列表。
|
||||
* @returns 捕获的退出码、标准输出与标准错误。
|
||||
* @throws 子进程无法启动时抛出。
|
||||
*/
|
||||
run(command, args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, { shell: false, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
||||
@@ -443,6 +460,12 @@ var OAuthCallbackServer = class {
|
||||
this.resolveResult = resolve;
|
||||
this.rejectResult = reject;
|
||||
});
|
||||
/**
|
||||
* 在配置的固定回调端口监听 `127.0.0.1`。
|
||||
*
|
||||
* @returns 端口绑定完成后解析的 Promise。
|
||||
* @throws 服务已运行或端口无法绑定时抛出。
|
||||
*/
|
||||
async listen() {
|
||||
if (this.server) throw new Error("OAuth callback server is already running");
|
||||
this.server = createServer((request, response) => {
|
||||
@@ -482,6 +505,13 @@ var OAuthCallbackServer = class {
|
||||
const address = this.server.address();
|
||||
if (!address || address.port !== this.port) throw new Error(`OAuth callback server did not bind port ${this.port}`);
|
||||
}
|
||||
/**
|
||||
* 等待有效回调或达到配置的超时时间。
|
||||
*
|
||||
* @param timeoutMs - 最大等待时间(毫秒)。
|
||||
* @returns 经过验证的回调中的授权码。
|
||||
* @throws 授权失败、状态验证失败、缺少授权码或等待超时时抛出。
|
||||
*/
|
||||
async wait(timeoutMs = 10 * 60 * 1e3) {
|
||||
let timeout;
|
||||
try {
|
||||
@@ -495,6 +525,11 @@ var OAuthCallbackServer = class {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 停止接收回调;对已停止的服务调用此方法不会产生任何操作。
|
||||
*
|
||||
* @returns HTTP 服务关闭后解析的 Promise。
|
||||
*/
|
||||
async close() {
|
||||
if (!this.server) return;
|
||||
const server = this.server;
|
||||
@@ -506,6 +541,13 @@ var OAuthCallbackServer = class {
|
||||
// src/oauthProvider.ts
|
||||
import { randomBytes, timingSafeEqual } from "node:crypto";
|
||||
var CraftTableOAuthProvider = class {
|
||||
/**
|
||||
* @param options - 远端端点与已注册公共客户端设置。
|
||||
* @param tokenStore - OAuth 令牌安全持久化接口。
|
||||
* @param discoveryStore - 授权服务器发现状态缓存。
|
||||
* @param onRedirect - 打开或显示授权 URL 的处理器。
|
||||
* @param state - 预期回调状态,可注入以实现确定性测试。
|
||||
*/
|
||||
constructor(options, tokenStore, discoveryStore, onRedirect, state = randomBytes(32).toString("base64url")) {
|
||||
this.options = options;
|
||||
this.tokenStore = tokenStore;
|
||||
@@ -532,40 +574,63 @@ var CraftTableOAuthProvider = class {
|
||||
account;
|
||||
expectedState;
|
||||
codeVerifierValue;
|
||||
/** @returns 授权回调必须携带的 state 值。 */
|
||||
state() {
|
||||
return this.expectedState;
|
||||
}
|
||||
/**
|
||||
* 在长度一致时以常量时间比较回调 state。
|
||||
*
|
||||
* @param value - 从 loopback 回调收到的 state。
|
||||
* @returns 回调是否属于本次授权尝试。
|
||||
*/
|
||||
validateState(value) {
|
||||
if (!value) return false;
|
||||
const expected = Buffer.from(this.expectedState);
|
||||
const actual = Buffer.from(value);
|
||||
return expected.length === actual.length && timingSafeEqual(expected, actual);
|
||||
}
|
||||
/** @returns MCP SDK 使用的静态公共客户端注册信息。 */
|
||||
clientInformation() {
|
||||
return { client_id: this.options.clientId };
|
||||
}
|
||||
/** @returns 当前服务/客户端组合已保存的 OAuth 令牌。 */
|
||||
tokens() {
|
||||
return this.tokenStore.get(this.account);
|
||||
}
|
||||
/** 持久化刷新或新签发的 OAuth 令牌集。 */
|
||||
saveTokens(tokens) {
|
||||
return this.tokenStore.set(this.account, tokens);
|
||||
}
|
||||
/** 将授权 URL 交给 CLI 的浏览器或手动登录处理器。 */
|
||||
redirectToAuthorization(url) {
|
||||
return this.onRedirect(url);
|
||||
}
|
||||
/** 在本次登录尝试期间将 PKCE verifier 保存在内存中。 */
|
||||
saveCodeVerifier(codeVerifier) {
|
||||
this.codeVerifierValue = codeVerifier;
|
||||
}
|
||||
/**
|
||||
* @returns 为当前授权尝试保存的 PKCE verifier。
|
||||
* @throws 尚未生成 verifier 或其已失效时抛出。
|
||||
*/
|
||||
codeVerifier() {
|
||||
if (!this.codeVerifierValue) throw new Error("OAuth PKCE verifier is missing or expired");
|
||||
return this.codeVerifierValue;
|
||||
}
|
||||
/** @returns 当前服务/客户端组合的 OAuth 发现缓存。 */
|
||||
discoveryState() {
|
||||
return this.discoveryStore.get(this.account);
|
||||
}
|
||||
/** 在凭据存储之外持久化 OAuth 发现状态。 */
|
||||
saveDiscoveryState(state) {
|
||||
return this.discoveryStore.set(this.account, state);
|
||||
}
|
||||
/**
|
||||
* 使 MCP OAuth 客户端指定的凭据材料失效。
|
||||
*
|
||||
* @param scope - 要清理的凭据类别。
|
||||
*/
|
||||
async invalidateCredentials(scope) {
|
||||
if (scope === "all" || scope === "tokens") await this.tokenStore.delete(this.account);
|
||||
if (scope === "all" || scope === "discovery") await this.discoveryStore.delete(this.account);
|
||||
@@ -637,14 +702,19 @@ ${url.toString()}`);
|
||||
}
|
||||
async function logoutRemote(options, tokenStore, discoveryStore, localOnly, fetchFn = fetch) {
|
||||
const account = credentialAccount(options);
|
||||
if (localOnly) {
|
||||
const hadCredential = await tokenStore.delete(account);
|
||||
await discoveryStore.delete(account);
|
||||
return { hadCredential, revoked: false };
|
||||
}
|
||||
const tokens = await tokenStore.get(account);
|
||||
if (tokens && !localOnly) {
|
||||
if (tokens) {
|
||||
const endpoint = await discoverRevocationEndpoint(options.url, fetchFn);
|
||||
await revokeTokens(endpoint, options.clientId, tokens, fetchFn);
|
||||
}
|
||||
await tokenStore.delete(account);
|
||||
await discoveryStore.delete(account);
|
||||
return { hadCredential: Boolean(tokens), revoked: Boolean(tokens && !localOnly) };
|
||||
return { hadCredential: Boolean(tokens), revoked: Boolean(tokens) };
|
||||
}
|
||||
async function connectWithTransport(options, transport, authentication) {
|
||||
const client = new Client({ name: "crafttable-mcp-cli", version: "0.1.3" });
|
||||
@@ -760,6 +830,11 @@ var KeyringTokenStore = class {
|
||||
this.entryFactory = entryFactory;
|
||||
}
|
||||
entryFactory;
|
||||
/**
|
||||
* @param account - 稳定凭据账户标识。
|
||||
* @returns 解码后的令牌集;不存在凭据时返回 `undefined`。
|
||||
* @throws 凭据存储不可用或已存数据无效时抛出。
|
||||
*/
|
||||
async get(account) {
|
||||
let serialized;
|
||||
try {
|
||||
@@ -787,6 +862,13 @@ var KeyringTokenStore = class {
|
||||
throw invalidCredentialError();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 原子发布新一代分块,并删除上一代分块。
|
||||
*
|
||||
* @param account - 稳定凭据账户标识。
|
||||
* @param tokens - 要安全存储的 OAuth 令牌。
|
||||
* @throws 任一凭据存储操作失败时抛出。
|
||||
*/
|
||||
async set(account, tokens) {
|
||||
const encoded = Buffer.from(JSON.stringify(tokens), "utf8").toString("base64");
|
||||
const chunks = splitCredential(encoded);
|
||||
@@ -811,6 +893,12 @@ var KeyringTokenStore = class {
|
||||
await Promise.allSettled(Array.from({ length: previousManifest.chunks }, (_, index) => this.deletePassword(chunkAccount(account, previousManifest.generation, index))));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 删除账户清单及其引用的全部分块。
|
||||
*
|
||||
* @param account - 稳定凭据账户标识。
|
||||
* @throws 无法访问凭据存储时抛出。
|
||||
*/
|
||||
async delete(account) {
|
||||
try {
|
||||
const serialized = await this.readPassword(account);
|
||||
@@ -819,13 +907,14 @@ var KeyringTokenStore = class {
|
||||
if (manifest) {
|
||||
await Promise.all(Array.from({ length: manifest.chunks }, (_, index) => this.deletePassword(chunkAccount(account, manifest.generation, index))));
|
||||
}
|
||||
return serialized !== void 0;
|
||||
} catch (error) {
|
||||
throw keyringError(error);
|
||||
}
|
||||
}
|
||||
async readPassword(account) {
|
||||
try {
|
||||
return await (await this.entryFactory(account)).getPassword();
|
||||
return await (await this.entryFactory(account)).getPassword() ?? void 0;
|
||||
} catch (error) {
|
||||
if (isMissingCredential(error)) return void 0;
|
||||
throw error;
|
||||
@@ -924,7 +1013,7 @@ withConnection(program.command("serve").description("Run the local stdio bridge"
|
||||
for (const operation of ["configure", "unconfigure"]) {
|
||||
withConnection(program.command(`${operation} <agent>`).description(`${operation === "configure" ? "Add" : "Remove"} the stdio bridge and CraftTable Skill in Codex, Claude Code, or OpenCode`)).option("--dry-run", "show the planned changes without writing").option("--force", "replace a conflicting entry without prompting").action(async (agent, flags) => {
|
||||
const cliEntry = fileURLToPath(import.meta.url);
|
||||
if (!cliEntry.endsWith(".js")) throw new Error("Agent configuration requires the built CLI; run `npm --prefix apps/mcp-client run build` first");
|
||||
if (!cliEntry.endsWith(".js")) throw new Error("Agent configuration requires the built CLI; run `npm --prefix mcp-client run build` first");
|
||||
const input = {
|
||||
target: agent,
|
||||
options: connectionOptions(flags),
|
||||
|
||||
Reference in New Issue
Block a user