import process from 'node:process'; import { FinanceClient } from './finance-client.js'; process.on('exit', (code) => { process.stderr.write(`[finwise-finance] process exit with code ${code}\n`); }); process.on('uncaughtException', (error) => { process.stderr.write(`[finwise-finance] uncaughtException: ${error.stack ?? error.message}\n`); }); process.on('unhandledRejection', (reason) => { process.stderr.write(`[finwise-finance] unhandledRejection: ${reason}\n`); }); class McpServer { constructor(options) { this.options = options; this.tools = new Map(); this.metadata = []; this.buffer = ''; this.expectedLength = null; this.initialized = false; for (const tool of options.tools) { if (this.tools.has(tool.name)) { throw new Error(`Duplicate MCP tool name: ${tool.name}`); } this.tools.set(tool.name, tool); this.metadata.push({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema, ...(tool.outputSchema ? { outputSchema: tool.outputSchema } : {}), }); } } start() { process.stdin.setEncoding('utf8'); process.stdin.on('data', (chunk) => { this.buffer += chunk; void this.drain(); }); process.stdin.on('end', () => { this.log('stdin ended'); }); process.stdin.on('close', () => { this.log('stdin closed'); }); process.stdin.resume(); this.log('MCP service ready'); } write(payload) { const json = JSON.stringify(payload); const frame = `Content-Length: ${Buffer.byteLength(json, 'utf8')}\r\n\r\n${json}`; process.stdout.write(frame); } respond(id, result) { this.log(`responding to ${id} with result`); if (id === undefined) return; this.write({ jsonrpc: '2.0', id, result }); } respondError(id, code, message) { this.log(`responding error to ${id}: [${code}] ${message}`); if (id === undefined) return; this.write({ jsonrpc: '2.0', id, error: { code, message } }); } notify(method, params) { this.log(`notifying ${method}`); this.write({ jsonrpc: '2.0', method, params }); } async drain() { while (true) { if (this.expectedLength === null) { const headerEnd = this.buffer.indexOf('\r\n\r\n'); if (headerEnd === -1) return; const header = this.buffer.slice(0, headerEnd); const match = header.match(/content-length:\s*(\d+)/i); if (!match) { this.buffer = this.buffer.slice(headerEnd + 4); continue; } this.expectedLength = Number.parseInt(match[1], 10); this.buffer = this.buffer.slice(headerEnd + 4); } if (this.buffer.length < (this.expectedLength ?? 0)) return; const body = this.buffer.slice(0, this.expectedLength ?? 0); this.buffer = this.buffer.slice(this.expectedLength ?? 0); this.expectedLength = null; await this.handleMessage(body); } } async handleMessage(payload) { this.log(`received payload: ${payload}`); let request; try { request = JSON.parse(payload); } catch { this.respondError(null, -32700, 'Parse error'); return; } if (!request || request.jsonrpc !== '2.0' || typeof request.method !== 'string') { this.respondError(request?.id, -32600, 'Invalid Request'); return; } try { await this.dispatch(request); } catch (error) { this.log(`Unexpected error: ${error.message}`); this.respondError(request.id, -32000, error.message); } } async dispatch(request) { switch (request.method) { case 'initialize': { if (this.initialized) { this.respondError(request.id, -32600, 'Already initialized'); return; } this.initialized = true; this.respond(request.id, { protocolVersion: '2024-10-07', capabilities: { tools: { list: true, call: true } }, service: { name: this.options.name, version: this.options.version, description: this.options.description, }, }); this.notify('notifications/ready', {}); return; } case 'tools/list': { this.assertInitialized('tools/list'); this.respond(request.id, { tools: this.metadata }); return; } case 'tools/call': { this.assertInitialized('tools/call'); const params = request.params ?? {}; const toolName = params.name; if (!toolName || typeof toolName !== 'string') { this.respondError(request.id, -32602, 'Tool name is required'); return; } const tool = this.tools.get(toolName); if (!tool) { this.respondError(request.id, -32601, `Unknown tool: ${toolName}`); return; } try { const result = await tool.handler(params.arguments ?? {}); this.respond(request.id, result); } catch (error) { this.respondError(request.id, -32001, error.message); } return; } case 'ping': { this.respond(request.id, 'pong'); return; } case 'shutdown': { this.respond(request.id, null); process.nextTick(() => process.exit(0)); return; } default: { this.respondError(request.id, -32601, `Method not found: ${request.method}`); } } } assertInitialized(method) { if (!this.initialized) { throw new Error(`Received ${method} before initialize`); } } log(message) { process.stderr.write(`[${this.options.name}] ${message}\n`); } } const jsonResult = (data) => ({ content: [ { type: 'application/json', data, }, ], }); const ensureNumber = (value, field) => { if (typeof value === 'number' && Number.isFinite(value)) return value; if (typeof value === 'string' && value.trim()) { const parsed = Number(value); if (!Number.isNaN(parsed)) return parsed; } throw new Error(`${field} must be a number`); }; const optionalNumber = (value, field) => { if (value === undefined || value === null) return undefined; return ensureNumber(value, field); }; const optionalNullableNumber = (value, field) => { if (value === undefined) return undefined; if (value === null) return null; if (typeof value === 'string') { const normalized = value.trim().toLowerCase(); if (!normalized || normalized === 'null') return null; } return ensureNumber(value, field); }; const ensureString = (value, field) => { if (typeof value === 'string') { const trimmed = value.trim(); if (!trimmed) throw new Error(`${field} cannot be empty`); return trimmed; } if (value === undefined || value === null) throw new Error(`${field} is required`); return ensureString(String(value), field); }; const optionalString = (value) => { if (value === undefined || value === null) return undefined; return String(value); }; const optionalNullableString = (value) => { if (value === undefined) return undefined; if (value === null) return null; const normalized = String(value).trim(); if (normalized.toLowerCase() === 'null') return null; return normalized; }; const optionalBoolean = (value, field) => { if (value === undefined || value === null) return undefined; if (typeof value === 'boolean') return value; if (typeof value === 'number') return value !== 0; if (typeof value === 'string') { const normalized = value.trim().toLowerCase(); if (['true', '1', 'yes', 'y'].includes(normalized)) return true; if (['false', '0', 'no', 'n'].includes(normalized)) return false; } throw new Error(`${field} must be boolean`); }; const parseStringArray = (value) => { if (value === undefined || value === null) return undefined; let items = []; if (Array.isArray(value)) { items = value.map((item) => String(item).trim()).filter(Boolean); } else if (typeof value === 'string') { items = value.split(',').map((item) => item.trim()).filter(Boolean); } else { items = [String(value).trim()].filter(Boolean); } return items.length ? items : undefined; }; const buildTransactionCreatePayload = (args, options = {}) => { const payload = { type: optionalString(args?.type) ?? options.defaultType ?? ensureString(args?.type, 'type'), amount: ensureNumber(args?.amount, 'amount'), currency: optionalString(args?.currency) ?? 'CNY', transactionDate: ensureString(args?.transactionDate, 'transactionDate'), }; const categoryId = optionalNullableNumber(args?.categoryId, 'categoryId'); if (categoryId !== undefined) payload.categoryId = categoryId; const accountId = optionalNullableNumber(args?.accountId, 'accountId'); if (accountId !== undefined) payload.accountId = accountId; const description = optionalString(args?.description); if (description !== undefined) payload.description = description; const project = optionalNullableString(args?.project); if (project !== undefined) payload.project = project; const memo = optionalNullableString(args?.memo); if (memo !== undefined) payload.memo = memo; const status = optionalString(args?.status); if (status !== undefined) payload.status = status; const reimbursementBatch = optionalNullableString(args?.reimbursementBatch); if (reimbursementBatch !== undefined) payload.reimbursementBatch = reimbursementBatch; const reviewNotes = optionalNullableString(args?.reviewNotes); if (reviewNotes !== undefined) payload.reviewNotes = reviewNotes; const submittedBy = optionalNullableString(args?.submittedBy); if (submittedBy !== undefined) payload.submittedBy = submittedBy; const approvedBy = optionalNullableString(args?.approvedBy); if (approvedBy !== undefined) payload.approvedBy = approvedBy; const approvedAt = optionalNullableString(args?.approvedAt); if (approvedAt !== undefined) payload.approvedAt = approvedAt; const statusUpdatedAt = optionalNullableString(args?.statusUpdatedAt); if (statusUpdatedAt !== undefined) payload.statusUpdatedAt = statusUpdatedAt; const isDeleted = optionalBoolean(args?.isDeleted, 'isDeleted'); if (isDeleted !== undefined) payload.isDeleted = isDeleted; return payload; }; const buildTransactionUpdatePayload = (args) => { const payload = {}; if (args?.type !== undefined) payload.type = ensureString(args.type, 'type'); if (args?.amount !== undefined) payload.amount = ensureNumber(args.amount, 'amount'); if (args?.currency !== undefined) payload.currency = ensureString(args.currency, 'currency'); if (args?.transactionDate !== undefined) payload.transactionDate = ensureString(args.transactionDate, 'transactionDate'); if (args?.categoryId !== undefined) payload.categoryId = optionalNullableNumber(args.categoryId, 'categoryId'); if (args?.accountId !== undefined) payload.accountId = optionalNullableNumber(args.accountId, 'accountId'); if (args?.description !== undefined) payload.description = args.description === null ? '' : String(args.description); if (args?.project !== undefined) payload.project = optionalNullableString(args.project) ?? null; if (args?.memo !== undefined) payload.memo = optionalNullableString(args.memo) ?? null; if (args?.status !== undefined) payload.status = ensureString(args.status, 'status'); if (args?.statusUpdatedAt !== undefined) payload.statusUpdatedAt = ensureString(args.statusUpdatedAt, 'statusUpdatedAt'); if (args?.reimbursementBatch !== undefined) payload.reimbursementBatch = optionalNullableString(args.reimbursementBatch) ?? null; if (args?.reviewNotes !== undefined) payload.reviewNotes = optionalNullableString(args.reviewNotes) ?? null; if (args?.submittedBy !== undefined) payload.submittedBy = optionalNullableString(args.submittedBy) ?? null; if (args?.approvedBy !== undefined) payload.approvedBy = optionalNullableString(args.approvedBy) ?? null; if (args?.approvedAt !== undefined) payload.approvedAt = optionalNullableString(args.approvedAt) ?? null; const isDeleted = optionalBoolean(args?.isDeleted, 'isDeleted'); if (isDeleted !== undefined) payload.isDeleted = isDeleted; return payload; }; const createFinanceTools = (client) => { const tools = []; tools.push({ name: 'finance_list_accounts', description: '列出账户,可选货币过滤', inputSchema: { type: 'object', additionalProperties: false, properties: { currency: { type: 'string', description: 'ISO 4217 货币代码' }, }, }, handler: async (args) => { const currency = optionalString(args?.currency); return jsonResult(await client.listAccounts(currency ? { currency } : {})); }, }); tools.push({ name: 'finance_list_budgets', description: '查询预算列表', inputSchema: { type: 'object', additionalProperties: false, properties: {} }, handler: async () => jsonResult(await client.listBudgets()), }); tools.push({ name: 'finance_create_budget', description: '创建预算', inputSchema: { type: 'object', additionalProperties: false, required: ['category', 'categoryId', 'limit', 'currency', 'period'], properties: { category: { type: 'string' }, categoryId: { type: 'number' }, emoji: { type: 'string' }, limit: { type: 'number' }, spent: { type: 'number' }, remaining: { type: 'number' }, percentage: { type: 'number' }, currency: { type: 'string' }, period: { type: 'string' }, alertThreshold: { type: 'number' }, description: { type: 'string' }, autoRenew: { type: 'boolean' }, overspendAlert: { type: 'boolean' }, dailyReminder: { type: 'boolean' }, monthlyTrend: { type: 'number' }, isDeleted: { type: 'boolean' }, }, }, handler: async (args) => { const payload = { category: ensureString(args?.category, 'category'), categoryId: ensureNumber(args?.categoryId, 'categoryId'), emoji: optionalString(args?.emoji), limit: ensureNumber(args?.limit, 'limit'), spent: optionalNumber(args?.spent, 'spent'), remaining: optionalNumber(args?.remaining, 'remaining'), percentage: optionalNumber(args?.percentage, 'percentage'), currency: ensureString(args?.currency, 'currency'), period: ensureString(args?.period, 'period'), alertThreshold: optionalNumber(args?.alertThreshold, 'alertThreshold'), description: optionalString(args?.description), autoRenew: optionalBoolean(args?.autoRenew, 'autoRenew'), overspendAlert: optionalBoolean(args?.overspendAlert, 'overspendAlert'), dailyReminder: optionalBoolean(args?.dailyReminder, 'dailyReminder'), monthlyTrend: optionalNumber(args?.monthlyTrend, 'monthlyTrend'), isDeleted: optionalBoolean(args?.isDeleted, 'isDeleted'), }; return jsonResult(await client.createBudget(payload)); }, }); tools.push({ name: 'finance_update_budget', description: '更新预算', inputSchema: { type: 'object', additionalProperties: false, required: ['id'], properties: { id: { type: 'number' }, category: { type: 'string' }, categoryId: { type: 'number' }, emoji: { type: 'string' }, limit: { type: 'number' }, spent: { type: 'number' }, remaining: { type: 'number' }, percentage: { type: 'number' }, currency: { type: 'string' }, period: { type: 'string' }, alertThreshold: { type: 'number' }, description: { type: 'string' }, autoRenew: { type: 'boolean' }, overspendAlert: { type: 'boolean' }, dailyReminder: { type: 'boolean' }, monthlyTrend: { type: 'number' }, isDeleted: { type: 'boolean' }, }, }, handler: async (args) => { const id = ensureNumber(args?.id, 'id'); const payload = {}; if (args?.category !== undefined) payload.category = ensureString(args.category, 'category'); if (args?.categoryId !== undefined) payload.categoryId = ensureNumber(args.categoryId, 'categoryId'); if (args?.emoji !== undefined) payload.emoji = optionalString(args.emoji); if (args?.limit !== undefined) payload.limit = ensureNumber(args.limit, 'limit'); if (args?.spent !== undefined) payload.spent = ensureNumber(args.spent, 'spent'); if (args?.remaining !== undefined) payload.remaining = ensureNumber(args.remaining, 'remaining'); if (args?.percentage !== undefined) payload.percentage = ensureNumber(args.percentage, 'percentage'); if (args?.currency !== undefined) payload.currency = ensureString(args.currency, 'currency'); if (args?.period !== undefined) payload.period = ensureString(args.period, 'period'); if (args?.alertThreshold !== undefined) payload.alertThreshold = ensureNumber(args.alertThreshold, 'alertThreshold'); if (args?.description !== undefined) payload.description = optionalString(args.description); const autoRenew = optionalBoolean(args?.autoRenew, 'autoRenew'); if (autoRenew !== undefined) payload.autoRenew = autoRenew; const overspendAlert = optionalBoolean(args?.overspendAlert, 'overspendAlert'); if (overspendAlert !== undefined) payload.overspendAlert = overspendAlert; const dailyReminder = optionalBoolean(args?.dailyReminder, 'dailyReminder'); if (dailyReminder !== undefined) payload.dailyReminder = dailyReminder; if (args?.monthlyTrend !== undefined) payload.monthlyTrend = ensureNumber(args.monthlyTrend, 'monthlyTrend'); const isDeleted = optionalBoolean(args?.isDeleted, 'isDeleted'); if (isDeleted !== undefined) payload.isDeleted = isDeleted; return jsonResult(await client.updateBudget(id, payload)); }, }); tools.push({ name: 'finance_delete_budget', description: '删除预算(软删)', inputSchema: { type: 'object', additionalProperties: false, required: ['id'], properties: { id: { type: 'number' } }, }, handler: async (args) => jsonResult(await client.deleteBudget(ensureNumber(args?.id, 'id'))), }); tools.push({ name: 'finance_list_categories', description: '查询分类,可按类型过滤', inputSchema: { type: 'object', additionalProperties: false, properties: { type: { type: 'string', description: 'expense / income' }, }, }, handler: async (args) => { const type = optionalString(args?.type); return jsonResult(await client.listCategories(type ? { type } : {})); }, }); tools.push({ name: 'finance_create_category', description: '创建分类', inputSchema: { type: 'object', additionalProperties: false, required: ['name', 'type'], properties: { name: { type: 'string' }, type: { type: 'string' }, icon: { type: 'string' }, color: { type: 'string' }, }, }, handler: async (args) => jsonResult( await client.createCategory({ name: ensureString(args?.name, 'name'), type: ensureString(args?.type, 'type'), icon: optionalString(args?.icon), color: optionalString(args?.color), }), ), }); tools.push({ name: 'finance_update_category', description: '更新分类', inputSchema: { type: 'object', additionalProperties: false, required: ['id'], properties: { id: { type: 'number' }, name: { type: 'string' }, icon: { type: 'string' }, color: { type: 'string' }, isActive: { type: 'boolean' }, }, }, handler: async (args) => { const id = ensureNumber(args?.id, 'id'); const payload = {}; if (args?.name !== undefined) payload.name = ensureString(args.name, 'name'); if (args?.icon !== undefined) payload.icon = optionalString(args.icon); if (args?.color !== undefined) payload.color = optionalString(args.color); const isActive = optionalBoolean(args?.isActive, 'isActive'); if (isActive !== undefined) payload.isActive = isActive; return jsonResult(await client.updateCategory(id, payload)); }, }); tools.push({ name: 'finance_delete_category', description: '删除分类(软删)', inputSchema: { type: 'object', additionalProperties: false, required: ['id'], properties: { id: { type: 'number' } }, }, handler: async (args) => jsonResult(await client.deleteCategory(ensureNumber(args?.id, 'id'))), }); tools.push({ name: 'finance_list_currencies', description: '列出可用货币', inputSchema: { type: 'object', additionalProperties: false, properties: {} }, handler: async () => jsonResult(await client.listCurrencies()), }); tools.push({ name: 'finance_list_exchange_rates', description: '查询汇率', inputSchema: { type: 'object', additionalProperties: false, properties: { fromCurrency: { type: 'string' }, toCurrency: { type: 'string' }, date: { type: 'string' }, }, }, handler: async (args) => { const params = {}; if (args?.fromCurrency !== undefined) params.fromCurrency = ensureString(args.fromCurrency, 'fromCurrency'); if (args?.toCurrency !== undefined) params.toCurrency = ensureString(args.toCurrency, 'toCurrency'); if (args?.date !== undefined) params.date = ensureString(args.date, 'date'); return jsonResult(await client.listExchangeRates(params)); }, }); tools.push({ name: 'finance_list_transactions', description: '查询交易列表', inputSchema: { type: 'object', additionalProperties: false, properties: { type: { type: 'string' }, statuses: { type: ['array', 'string'], items: { type: 'string' } }, includeDeleted: { type: 'boolean' }, }, }, handler: async (args) => { const type = optionalString(args?.type); const statuses = parseStringArray(args?.statuses); const includeDeleted = optionalBoolean(args?.includeDeleted, 'includeDeleted'); return jsonResult( await client.listTransactions({ ...(type ? { type } : {}), ...(statuses ? { statuses } : {}), ...(includeDeleted !== undefined ? { includeDeleted } : {}), }), ); }, }); tools.push({ name: 'finance_create_transaction', description: '创建交易', inputSchema: { type: 'object', additionalProperties: false, required: ['type', 'amount', 'transactionDate'], properties: { type: { type: 'string' }, amount: { type: 'number' }, currency: { type: 'string' }, categoryId: { type: ['number', 'null'] }, accountId: { type: ['number', 'null'] }, transactionDate: { type: 'string' }, description: { type: 'string' }, project: { type: ['string', 'null'] }, memo: { type: ['string', 'null'] }, status: { type: 'string' }, reimbursementBatch: { type: ['string', 'null'] }, reviewNotes: { type: ['string', 'null'] }, submittedBy: { type: ['string', 'null'] }, approvedBy: { type: ['string', 'null'] }, approvedAt: { type: ['string', 'null'] }, statusUpdatedAt: { type: ['string', 'null'] }, isDeleted: { type: 'boolean' }, }, }, handler: async (args) => jsonResult(await client.createTransaction(buildTransactionCreatePayload(args))), }); tools.push({ name: 'finance_update_transaction', description: '更新交易', inputSchema: { type: 'object', additionalProperties: false, required: ['id'], properties: { id: { type: 'number' }, type: { type: 'string' }, amount: { type: 'number' }, currency: { type: 'string' }, categoryId: { type: ['number', 'null'] }, accountId: { type: ['number', 'null'] }, transactionDate: { type: 'string' }, description: { type: ['string', 'null'] }, project: { type: ['string', 'null'] }, memo: { type: ['string', 'null'] }, status: { type: 'string' }, statusUpdatedAt: { type: 'string' }, reimbursementBatch: { type: ['string', 'null'] }, reviewNotes: { type: ['string', 'null'] }, submittedBy: { type: ['string', 'null'] }, approvedBy: { type: ['string', 'null'] }, approvedAt: { type: ['string', 'null'] }, isDeleted: { type: 'boolean' }, }, }, handler: async (args) => jsonResult( await client.updateTransaction(ensureNumber(args?.id, 'id'), buildTransactionUpdatePayload(args)), ), }); tools.push({ name: 'finance_delete_transaction', description: '删除交易(软删)', inputSchema: { type: 'object', additionalProperties: false, required: ['id'], properties: { id: { type: 'number' } }, }, handler: async (args) => jsonResult(await client.deleteTransaction(ensureNumber(args?.id, 'id'))), }); tools.push({ name: 'finance_list_reimbursements', description: '查询报销单', inputSchema: { type: 'object', additionalProperties: false, properties: { type: { type: 'string' }, statuses: { type: ['array', 'string'], items: { type: 'string' } }, includeDeleted: { type: 'boolean' }, }, }, handler: async (args) => { const type = optionalString(args?.type); const statuses = parseStringArray(args?.statuses); const includeDeleted = optionalBoolean(args?.includeDeleted, 'includeDeleted'); return jsonResult( await client.listReimbursements({ ...(type ? { type } : {}), ...(statuses ? { statuses } : {}), ...(includeDeleted !== undefined ? { includeDeleted } : {}), }), ); }, }); tools.push({ name: 'finance_create_reimbursement', description: '创建报销单', inputSchema: { type: 'object', additionalProperties: false, required: ['amount', 'transactionDate'], properties: { type: { type: 'string' }, amount: { type: 'number' }, currency: { type: 'string' }, categoryId: { type: ['number', 'null'] }, accountId: { type: ['number', 'null'] }, transactionDate: { type: 'string' }, description: { type: 'string' }, project: { type: ['string', 'null'] }, memo: { type: ['string', 'null'] }, status: { type: 'string' }, reimbursementBatch: { type: ['string', 'null'] }, reviewNotes: { type: ['string', 'null'] }, submittedBy: { type: ['string', 'null'] }, approvedBy: { type: ['string', 'null'] }, approvedAt: { type: ['string', 'null'] }, statusUpdatedAt: { type: ['string', 'null'] }, isDeleted: { type: 'boolean' }, }, }, handler: async (args) => jsonResult( await client.createReimbursement(buildTransactionCreatePayload(args, { defaultType: 'expense' })), ), }); tools.push({ name: 'finance_update_reimbursement', description: '更新报销单', inputSchema: { type: 'object', additionalProperties: false, required: ['id'], properties: { id: { type: 'number' }, type: { type: 'string' }, amount: { type: 'number' }, currency: { type: 'string' }, categoryId: { type: ['number', 'null'] }, accountId: { type: ['number', 'null'] }, transactionDate: { type: 'string' }, description: { type: ['string', 'null'] }, project: { type: ['string', 'null'] }, memo: { type: ['string', 'null'] }, status: { type: 'string' }, statusUpdatedAt: { type: 'string' }, reimbursementBatch: { type: ['string', 'null'] }, reviewNotes: { type: ['string', 'null'] }, submittedBy: { type: ['string', 'null'] }, approvedBy: { type: ['string', 'null'] }, approvedAt: { type: ['string', 'null'] }, isDeleted: { type: 'boolean' }, }, }, handler: async (args) => jsonResult( await client.updateReimbursement( ensureNumber(args?.id, 'id'), buildTransactionUpdatePayload(args), ), ), }); tools.push({ name: 'finance_list_media', description: '查询媒体消息', inputSchema: { type: 'object', additionalProperties: false, properties: { limit: { type: 'number' }, fileTypes: { type: ['array', 'string'], items: { type: 'string' } }, }, }, handler: async (args) => { const limit = optionalNumber(args?.limit, 'limit'); const fileTypes = parseStringArray(args?.fileTypes); return jsonResult( await client.listMedia({ ...(limit !== undefined ? { limit } : {}), ...(fileTypes ? { fileTypes } : {}), }), ); }, }); tools.push({ name: 'finance_get_media', description: '根据 ID 获取媒体详情', inputSchema: { type: 'object', additionalProperties: false, required: ['id'], properties: { id: { type: 'number' } }, }, handler: async (args) => jsonResult(await client.getMediaById(ensureNumber(args?.id, 'id'))), }); tools.push({ name: 'finance_download_media', description: '下载媒体文件并返回 Base64', inputSchema: { type: 'object', additionalProperties: false, required: ['id'], properties: { id: { type: 'number' }, includeMetadata: { type: 'boolean', default: true }, }, }, outputSchema: { type: 'object', properties: { fileName: { type: ['string', 'null'] }, mimeType: { type: 'string' }, size: { type: 'number' }, base64: { type: 'string' }, metadata: { type: ['object', 'null'] }, }, }, handler: async (args) => { const id = ensureNumber(args?.id, 'id'); const includeMetadata = optionalBoolean(args?.includeMetadata, 'includeMetadata'); const file = await client.downloadMedia(id); const metadata = includeMetadata === false ? null : await client.getMediaById(id); return jsonResult({ ...file, metadata }); }, }); return tools; }; const createServer = () => { const baseUrl = process.env.FINANCE_API_BASE_URL ?? 'http://172.16.74.149:5666'; const apiKey = process.env.FINANCE_API_KEY; const timeoutEnv = process.env.FINANCE_API_TIMEOUT; const timeout = timeoutEnv ? Number.parseInt(timeoutEnv, 10) : undefined; const basicUsername = process.env.FINANCE_BASIC_USERNAME ?? process.env.FINANCE_BASIC_USER ?? process.env.FINANCE_USERNAME; const basicPassword = process.env.FINANCE_BASIC_PASSWORD ?? process.env.FINANCE_PASSWORD; const basicAuth = basicUsername && basicPassword ? { username: basicUsername, password: basicPassword } : undefined; const client = new FinanceClient({ baseUrl, apiKey, basicAuth, timeoutMs: Number.isFinite(timeout ?? NaN) ? timeout : undefined, }); return new McpServer({ name: 'finwise-finance', version: '0.1.0', description: 'Finwise Pro 财务接口 MCP 服务', tools: createFinanceTools(client), }); }; createServer().start();