diff --git a/feedback.js b/feedback.js index 0a55a8b..6eb79ad 100644 --- a/feedback.js +++ b/feedback.js @@ -12,6 +12,7 @@ // submit(input, { ip, userAgent, via }) the same for an MCP tool; { ok, status, body } // mcpTool name, title, description and JSON schema for the MCP tool // mcpShape(z) the input as a zod shape, for McpServer.registerTool +// mcpOutputShape(z) the answer to a received report as a zod object (outputSchema) // mcpCall(args, { ip, userAgent }) the tool's CallToolResult // The usage log's own MCP describer should skip the "feedback" tool: submit() // already records it, with the full message. @@ -141,13 +142,23 @@ function createFeedback({ service, record = () => {}, agentOf = (ua) => String(u function mcpCall(args, { ip, userAgent } = {}) { const out = submit(args, { ip, userAgent, via: 'mcp' }); - return { content: [{ type: 'text', text: JSON.stringify(out.body) }], ...(out.ok ? {} : { isError: true }) }; + return { content: [{ type: 'text', text: JSON.stringify(out.body) }], ...(out.ok ? { structuredContent: out.body } : { isError: true }) }; } + // The answer to a report that was received, as a zod object (the tool's outputSchema). + const mcpOutputShape = (z) => z.object({ + received: z.boolean().describe('true: a person will read it'), + id: z.string().describe('Report id, to refer to it later'), + service: z.string().optional(), + type: z.string().optional(), + note: z.string().optional(), + }).passthrough(); + return { router, submit, mcpShape, + mcpOutputShape, mcpCall, mcpTool: { name: 'feedback', title: 'Send feedback (free)', description: DESCRIPTION, inputSchema: SCHEMA }, }; diff --git a/server.js b/server.js index c845a65..ce86c4e 100644 --- a/server.js +++ b/server.js @@ -590,6 +590,17 @@ app.post('/api/demo-explain', async (req, res) => { // --- Free MCP tools, rate-limited (see /api/check-wallet and /api/explain // above for the paid HTTP equivalents) --- +// What both tools answer: the verdict and the explanation, as text ("VERDICT: explanation", which the usage log +// reads) and as structuredContent matching this schema. Extra fields are allowed, so new ones never break a call. +const VERDICT_OUTPUT = z.object({ + verdict: z.string().describe('SAFE, CAUTION or RISK'), + explanation: z.string().describe('Plain-language explanation of what the approval or payload allows') +}).passthrough(); +const verdictResult = ({ verdict, explanation }) => ({ + content: [{ type: 'text', text: `${verdict}: ${explanation}` }], + structuredContent: { verdict, explanation } +}); + function buildMcpServer() { const server = new McpServer({ name: 'plaintext-wallet-checker', version: '1.0.0' }); @@ -602,12 +613,12 @@ function buildMcpServer() { address: z.string().regex(ADDRESS_RE).describe('EVM wallet address to check, e.g. 0x...'), chain: z.enum(Object.keys(CHAINS)).optional().describe('Chain to check approvals on (default: ethereum)'), kind: z.enum(['token', 'nft']).optional().describe('Approval type to check (default: token)') - }) + }), + outputSchema: VERDICT_OUTPUT }, async ({ address, chain, kind }) => { try { - const result = await getWalletVerdict({ address, chain, kind }); - return { content: [{ type: 'text', text: `${result.verdict}: ${result.explanation}` }] }; + return verdictResult(await getWalletVerdict({ address, chain, kind })); } catch (err) { return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true }; } @@ -621,7 +632,8 @@ function buildMcpServer() { description: "Explain an arbitrary approval/permission JSON payload in plain language, when you already have the data instead of needing an on-chain lookup.", inputSchema: z.object({ data: z.record(z.string(), z.any()).describe('Arbitrary approval/permission JSON payload to explain') - }) + }), + outputSchema: VERDICT_OUTPUT }, async ({ data }) => { try { @@ -629,8 +641,7 @@ function buildMcpServer() { if (size > MAX_EXPLAIN_PAYLOAD_CHARS) { return { content: [{ type: 'text', text: `Error: payload too large (${size} chars, max ${MAX_EXPLAIN_PAYLOAD_CHARS}).` }], isError: true }; } - const { verdict, explanation } = await explainPayload(data); - return { content: [{ type: 'text', text: `${verdict}: ${explanation}` }] }; + return verdictResult(await explainPayload(data)); } catch (err) { return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true }; } @@ -642,7 +653,8 @@ function buildMcpServer() { { title: feedback.mcpTool.title, description: feedback.mcpTool.description, - inputSchema: z.object(feedback.mcpShape(z)) + inputSchema: z.object(feedback.mcpShape(z)), + ...(feedback.mcpOutputShape ? { outputSchema: feedback.mcpOutputShape(z) } : {}) }, async (args) => feedback.mcpCall(args, mcpCaller.getStore() || {}) );