Files
dingMCP/scripts/xft-mcp-readonly-matrix.mjs
gitea-admin 504145aec6
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
feat: initial public release
2026-08-17 03:04:09 +00:00

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; });
}