#!/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 "); } 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; }); }