CI / Client installer (macos-latest) (push) Canceled after 0s
CI / Client installer (ubuntu-latest) (push) Canceled after 0s
CI / Client installer (windows-latest) (push) Canceled after 0s
CI / test (22) (push) Canceled after 0s
CI / test (24) (push) Canceled after 0s
CI / release-smoke (push) Canceled after 0s
75 lines
3.9 KiB
JavaScript
75 lines
3.9 KiB
JavaScript
#!/usr/bin/env node
|
|
import { readFileSync } from "node:fs";
|
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
import { createApprovalMcpServer } from "../dist/src/server.js";
|
|
import { XftOpenApiClient } from "../dist/src/xft-open-api.js";
|
|
|
|
const [appId, secretFile, enterpriseId, dingTalkUserId] = process.argv.slice(2);
|
|
if (!appId || !secretFile || !enterpriseId || !dingTalkUserId) {
|
|
throw new Error("usage: xft-mcp-readonly-matrix.mjs <appId> <secretFile> <enterpriseId> <dingTalkUserId>");
|
|
}
|
|
const lines = readFileSync(secretFile, "utf8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
const config = {
|
|
appMode: "gateway",
|
|
clientId: "readonly-mcp", clientSecret: "readonly-mcp",
|
|
apiBaseUrl: "https://api.dingtalk.com", oapiBaseUrl: "https://oapi.dingtalk.com",
|
|
publicBaseUrl: "https://readonly.invalid", port: 3000, requestTimeoutMs: 15_000,
|
|
officialLookbackDays: 120, officialScanConcurrency: 2, officialClientOnlyProcessCodes: [],
|
|
batchLimit: 20, approvalRefSecret: "readonly-matrix-reference-secret",
|
|
xftOpenApi: {
|
|
appId, authoritySecret: lines.length >= 2 ? lines[1] : lines[0], enterpriseId,
|
|
apiBaseUrl: "https://api.cmbchina.com", encrypted: true, dingTalkAppCode: "DDingTalk",
|
|
},
|
|
};
|
|
const xft = new XftOpenApiClient(config);
|
|
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
const server = createApprovalMcpServer(
|
|
config,
|
|
{ async listMyTodos() { throw new Error("DingTalk source must not be called"); } },
|
|
xft,
|
|
undefined,
|
|
undefined,
|
|
{ userId: dingTalkUserId },
|
|
);
|
|
const client = new Client({ name: "xft-readonly-matrix", version: "1.0.0" });
|
|
await server.connect(serverTransport);
|
|
await client.connect(clientTransport);
|
|
try {
|
|
const listed = result(await client.callTool({ name: "approval_list_todos", arguments: { source: "xft", query: "", limit: 100 } }));
|
|
const todos = Array.isArray(listed.items) ? listed.items : [];
|
|
const selected = distinctBy(todos, (item) => `${item.title ?? ""}|${item.amount?.value ?? ""}`).slice(0, 8);
|
|
const flows = [];
|
|
for (const todo of selected) {
|
|
try {
|
|
const summary = result(await client.callTool({ name: "approval_get_detail", arguments: { approvalId: todo.approvalId, view: "summary" } }));
|
|
const expense = result(await client.callTool({ name: "approval_get_detail", arguments: { approvalId: todo.approvalId, view: "expense" } }));
|
|
const actions = result(await client.callTool({ name: "approval_get_actions", arguments: { approvalId: todo.approvalId } }));
|
|
flows.push({
|
|
businessNo: todo.businessNo, title: todo.title, applicant: todo.applicant, amount: todo.amount, status: "ok",
|
|
summaryFields: Array.isArray(summary.detail?.formFields) ? summary.detail.formFields.length : 0,
|
|
expenseItems: Array.isArray(expense.detail?.expenseItems) ? expense.detail.expenseItems.length : 0,
|
|
actions: Array.isArray(actions.actions) ? actions.actions.map((action) => ({ action: action.action, allowed: action.allowed })) : [],
|
|
rawOmitted: summary.diagnostics?.rawOmitted === true,
|
|
});
|
|
} catch (error) {
|
|
flows.push({ businessNo: todo.businessNo, title: todo.title, status: "failed", error: error instanceof Error ? error.message : String(error) });
|
|
}
|
|
}
|
|
console.log(JSON.stringify({ totalTodos: todos.length, testedFlows: flows.length, sourceStatus: listed.sourceStatus, flows }, null, 2));
|
|
} finally {
|
|
await client.close();
|
|
await server.close();
|
|
}
|
|
|
|
function result(call) {
|
|
const value = call?.structuredContent?.result;
|
|
if (!value || typeof value !== "object") throw new Error("MCP tool did not return structuredContent.result");
|
|
return value;
|
|
}
|
|
|
|
function distinctBy(items, key) {
|
|
const seen = new Set();
|
|
return items.filter((item) => { const value = key(item); if (seen.has(value)) return false; seen.add(value); return true; });
|
|
}
|