From aadc0a9935bff1d616d0bef5145bba2a35d93aa8 Mon Sep 17 00:00:00 2001 From: ak68a Date: Wed, 25 Mar 2026 21:45:32 -0500 Subject: [PATCH 1/7] feat: add MCP server for ACK-ID and ACK-Pay operations Expose 9 tools via stdio MCP transport: identity (create/sign/verify credentials, resolve DIDs), payments (create/verify payment requests and receipts), and keypair generation. Signing tools accept JWK to prevent curve mismatches. MCP SDK pinned to 1.21.2 for zod 3 compatibility. --- pnpm-lock.yaml | 282 ++++++++++++++++++ tools/mcp-server/package.json | 36 +++ tools/mcp-server/src/index.ts | 28 ++ tools/mcp-server/src/tools/identity.test.ts | 71 +++++ tools/mcp-server/src/tools/identity.ts | 129 ++++++++ .../mcp-server/src/tools/payment-receipts.ts | 86 ++++++ .../src/tools/payment-requests.test.ts | 85 ++++++ .../mcp-server/src/tools/payment-requests.ts | 123 ++++++++ tools/mcp-server/src/tools/utility.ts | 40 +++ tools/mcp-server/src/util.test.ts | 97 ++++++ tools/mcp-server/src/util.ts | 69 +++++ tools/mcp-server/tsconfig.json | 8 + tools/mcp-server/vitest.config.ts | 7 + 13 files changed, 1061 insertions(+) create mode 100644 tools/mcp-server/package.json create mode 100644 tools/mcp-server/src/index.ts create mode 100644 tools/mcp-server/src/tools/identity.test.ts create mode 100644 tools/mcp-server/src/tools/identity.ts create mode 100644 tools/mcp-server/src/tools/payment-receipts.ts create mode 100644 tools/mcp-server/src/tools/payment-requests.test.ts create mode 100644 tools/mcp-server/src/tools/payment-requests.ts create mode 100644 tools/mcp-server/src/tools/utility.ts create mode 100644 tools/mcp-server/src/util.test.ts create mode 100644 tools/mcp-server/src/util.ts create mode 100644 tools/mcp-server/tsconfig.json create mode 100644 tools/mcp-server/vitest.config.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3197eec..b09aa8e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -602,6 +602,22 @@ importers: specifier: 1.7.0 version: 1.7.0 + tools/mcp-server: + dependencies: + '@modelcontextprotocol/sdk': + specifier: 1.21.2 + version: 1.21.2 + agentcommercekit: + specifier: workspace:* + version: link:../../packages/agentcommercekit + zod: + specifier: 'catalog:' + version: 3.25.4 + devDependencies: + '@repo/typescript-config': + specifier: workspace:* + version: link:../typescript-config + tools/typescript-config: {} packages: @@ -1693,6 +1709,15 @@ packages: '@mintlify/validation@0.1.606': resolution: {integrity: sha512-h9dg2g0qhaRkpYTS+LRDo/CVTlQN+TScBRcT0ah5NvXcrBNWPzGbKjJwFIbKn81+9yWx5l5XEea3Elic2imK5Q==} + '@modelcontextprotocol/sdk@1.21.2': + resolution: {integrity: sha512-HXR5NeVbaL45KuPRqfBQL/hcdc8Y197ALj5G75M5qUMcOk2at0bj2Nns4ZnjU2mTw52360TK63oDqvRjc1iPRQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@multiformats/base-x@4.0.1': resolution: {integrity: sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw==} @@ -3081,6 +3106,10 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -3379,6 +3408,10 @@ packages: resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} engines: {node: '>=18'} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -3583,6 +3616,10 @@ packages: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} @@ -3594,6 +3631,10 @@ packages: cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + cookie@0.4.2: resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} engines: {node: '>= 0.6'} @@ -4117,6 +4158,14 @@ packages: events-universal@1.0.1: resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -4125,6 +4174,12 @@ packages: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} + express-rate-limit@7.5.1: + resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + express@4.18.2: resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} engines: {node: '>= 0.10.0'} @@ -4133,6 +4188,10 @@ packages: resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -4209,6 +4268,10 @@ packages: resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -4254,6 +4317,10 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + front-matter@4.0.2: resolution: {integrity: sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==} @@ -4494,6 +4561,10 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -4718,6 +4789,9 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -5013,6 +5087,10 @@ packages: merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -5155,6 +5233,10 @@ packages: resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} @@ -5257,6 +5339,10 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + neotraverse@0.6.18: resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==} engines: {node: '>= 10'} @@ -5498,6 +5584,9 @@ packages: path-to-regexp@0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -5531,6 +5620,10 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + pony-cause@1.1.1: resolution: {integrity: sha512-PxkIc/2ZpLiEzQXu5YRDOUgBlfGYBY8156HY5ZcRAwwonMk5W/MrJP2LLkG/hF7GEQzaHo2aS7ho6ZLCOvf+6g==} engines: {node: '>=12.0.0'} @@ -5643,6 +5736,10 @@ packages: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} + qs@6.15.0: + resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + engines: {node: '>=0.6'} + quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} @@ -5916,6 +6013,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-async@3.0.0: resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==} engines: {node: '>=0.12.0'} @@ -5981,6 +6082,10 @@ packages: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + serialize-error@12.0.0: resolution: {integrity: sha512-ZYkZLAvKTKQXWuh5XpBw7CdbSzagarX39WyZ2H07CDLC5/KfsRGlIXV8d4+tfqX1M7916mRqR1QfNHSij+c9Pw==} engines: {node: '>=18'} @@ -5993,6 +6098,10 @@ packages: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -6152,6 +6261,10 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} @@ -6863,6 +6976,11 @@ packages: peerDependencies: zod: ^3.24.1 + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + zod@3.21.4: resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==} @@ -8254,6 +8372,24 @@ snapshots: - supports-color - typescript + '@modelcontextprotocol/sdk@1.21.2': + dependencies: + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + content-type: 1.0.5 + cors: 2.8.5 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 7.5.1(express@5.2.1) + pkce-challenge: 5.0.1 + raw-body: 3.0.1 + zod: 3.25.76 + zod-to-json-schema: 3.25.1(zod@3.25.76) + transitivePeerDependencies: + - supports-color + '@multiformats/base-x@4.0.1': {} '@napi-rs/wasm-runtime@1.1.1': @@ -9609,6 +9745,11 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + acorn-jsx@5.3.2(acorn@8.11.2): dependencies: acorn: 8.11.2 @@ -9922,6 +10063,20 @@ snapshots: transitivePeerDependencies: - supports-color + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.0 + on-finished: 2.4.1 + qs: 6.15.0 + raw-body: 3.0.1 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -10114,12 +10269,16 @@ snapshots: dependencies: safe-buffer: 5.2.1 + content-disposition@1.0.1: {} + content-type@1.0.5: {} convert-to-spaces@2.0.1: {} cookie-signature@1.0.6: {} + cookie-signature@1.2.2: {} + cookie@0.4.2: {} cookie@0.5.0: {} @@ -10669,11 +10828,21 @@ snapshots: transitivePeerDependencies: - bare-abort-controller + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + expand-template@2.0.3: optional: true expect-type@1.2.2: {} + express-rate-limit@7.5.1(express@5.2.1): + dependencies: + express: 5.2.1 + express@4.18.2: dependencies: accepts: 1.3.8 @@ -10746,6 +10915,39 @@ snapshots: transitivePeerDependencies: - supports-color + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + extend@3.0.2: {} extendable-error@0.1.7: {} @@ -10840,6 +11042,17 @@ snapshots: transitivePeerDependencies: - supports-color + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -10876,6 +11089,8 @@ snapshots: fresh@0.5.2: {} + fresh@2.0.0: {} + front-matter@4.0.2: dependencies: js-yaml: 3.14.1 @@ -11274,6 +11489,14 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -11505,6 +11728,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-promise@4.0.0: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -11935,6 +12160,8 @@ snapshots: merge-descriptors@1.0.3: {} + merge-descriptors@2.0.0: {} + merge2@1.4.1: {} methods@1.1.2: {} @@ -12251,6 +12478,10 @@ snapshots: dependencies: mime-db: 1.54.0 + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + mime@1.6.0: {} mimic-fn@2.1.0: {} @@ -12340,6 +12571,8 @@ snapshots: negotiator@0.6.3: {} + negotiator@1.0.0: {} + neotraverse@0.6.18: {} netmask@2.0.2: {} @@ -12634,6 +12867,8 @@ snapshots: path-to-regexp@0.1.7: {} + path-to-regexp@8.3.0: {} + path-type@4.0.0: {} pathe@2.0.3: {} @@ -12652,6 +12887,8 @@ snapshots: pirates@4.0.7: {} + pkce-challenge@5.0.1: {} + pony-cause@1.1.1: {} possible-typed-array-names@1.1.0: {} @@ -12792,6 +13029,10 @@ snapshots: dependencies: side-channel: 1.1.0 + qs@6.15.0: + dependencies: + side-channel: 1.1.0 + quansync@0.2.11: {} quansync@1.0.0: {} @@ -13244,6 +13485,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.52.5 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + run-async@3.0.0: {} run-parallel@1.2.0: @@ -13331,6 +13582,22 @@ snapshots: transitivePeerDependencies: - supports-color + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + serialize-error@12.0.0: dependencies: type-fest: 4.41.0 @@ -13353,6 +13620,15 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -13572,6 +13848,8 @@ snapshots: statuses@2.0.1: {} + statuses@2.0.2: {} + std-env@3.10.0: {} stop-iteration-iterator@1.1.0: @@ -14379,6 +14657,10 @@ snapshots: dependencies: zod: 3.25.76 + zod-to-json-schema@3.25.1(zod@3.25.76): + dependencies: + zod: 3.25.76 + zod@3.21.4: {} zod@3.23.8: {} diff --git a/tools/mcp-server/package.json b/tools/mcp-server/package.json new file mode 100644 index 0000000..1a5c782 --- /dev/null +++ b/tools/mcp-server/package.json @@ -0,0 +1,36 @@ +{ + "name": "@repo/mcp-server", + "version": "0.0.1", + "private": true, + "homepage": "https://github.com/agentcommercekit/ack#readme", + "bugs": "https://github.com/agentcommercekit/ack/issues", + "license": "MIT", + "author": { + "name": "Catena Labs", + "url": "https://catenalabs.com" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/agentcommercekit/ack.git", + "directory": "tools/mcp-server" + }, + "bin": { + "ack-mcp": "./src/index.ts" + }, + "type": "module", + "main": "./src/index.ts", + "scripts": { + "check:types": "tsc --noEmit", + "clean": "git clean -fdX .turbo", + "start": "tsx ./src/index.ts", + "test": "vitest" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "1.21.2", + "agentcommercekit": "workspace:*", + "zod": "catalog:" + }, + "devDependencies": { + "@repo/typescript-config": "workspace:*" + } +} diff --git a/tools/mcp-server/src/index.ts b/tools/mcp-server/src/index.ts new file mode 100644 index 0000000..a2a073c --- /dev/null +++ b/tools/mcp-server/src/index.ts @@ -0,0 +1,28 @@ +#!/usr/bin/env node +/** + * ACK MCP Server + * + * Exposes Agent Commerce Kit operations as MCP tools, enabling any + * MCP-compatible AI agent to create credentials, verify identities, + * issue payment requests, and verify receipts. + */ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" + +import { registerIdentityTools } from "./tools/identity" +import { registerPaymentReceiptTools } from "./tools/payment-receipts" +import { registerPaymentRequestTools } from "./tools/payment-requests" +import { registerUtilityTools } from "./tools/utility" + +const server = new McpServer({ + name: "ack", + version: "0.0.1", +}) + +registerIdentityTools(server) +registerPaymentRequestTools(server) +registerPaymentReceiptTools(server) +registerUtilityTools(server) + +const transport = new StdioServerTransport() +await server.connect(transport) diff --git a/tools/mcp-server/src/tools/identity.test.ts b/tools/mcp-server/src/tools/identity.test.ts new file mode 100644 index 0000000..abcd68c --- /dev/null +++ b/tools/mcp-server/src/tools/identity.test.ts @@ -0,0 +1,71 @@ +import { + createControllerCredential, + createDidKeyUri, + createJwtSigner, + generateKeypair, + keypairToJwk, + signCredential, + type DidUri, +} from "agentcommercekit" +import { describe, expect, it } from "vitest" + +import { curveToAlg } from "../util" + +describe("identity tool operations", () => { + it("creates a controller credential with correct structure", () => { + const credential = createControllerCredential({ + subject: "did:key:z6MkSubject" as DidUri, + controller: "did:key:z6MkController" as DidUri, + }) + + expect(credential.type).toContain("ControllerCredential") + expect(credential.issuer).toEqual({ id: "did:key:z6MkController" }) + expect(credential.credentialSubject.controller).toBe( + "did:key:z6MkController", + ) + }) + + it("signs a credential and produces a valid JWT", async () => { + const keypair = await generateKeypair("secp256k1") + const did = createDidKeyUri(keypair) + const signer = createJwtSigner(keypair) + + const credential = createControllerCredential({ + subject: "did:key:z6MkSubject" as DidUri, + controller: did, + }) + + const jwt = await signCredential(credential, { + did, + signer, + alg: curveToAlg(keypair.curve), + }) + + expect(jwt).toMatch(/^eyJ/) + expect(jwt.split(".")).toHaveLength(3) + }) + + it("round-trips a keypair through JWK for signing", async () => { + const keypair = await generateKeypair("secp256k1") + const did = createDidKeyUri(keypair) + const jwk = keypairToJwk(keypair) + + // Simulate what the MCP tool does: reconstruct from JWK + const { jwkToKeypair } = await import("agentcommercekit") + const restored = jwkToKeypair(jwk) + const signer = createJwtSigner(restored) + + const credential = createControllerCredential({ + subject: "did:key:z6MkSubject" as DidUri, + controller: did, + }) + + const jwt = await signCredential(credential, { + did, + signer, + alg: curveToAlg(restored.curve), + }) + + expect(jwt).toMatch(/^eyJ/) + }) +}) diff --git a/tools/mcp-server/src/tools/identity.ts b/tools/mcp-server/src/tools/identity.ts new file mode 100644 index 0000000..97722e8 --- /dev/null +++ b/tools/mcp-server/src/tools/identity.ts @@ -0,0 +1,129 @@ +/** + * ACK-ID identity tools for MCP. + */ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" +import { + createControllerCredential, + createJwtSigner, + parseJwtCredential, + resolveDid, + signCredential, + verifyParsedCredential, + type DidUri, + type JwtString, +} from "agentcommercekit" +import { z } from "zod" + +import { + curveToAlg, + err, + keypairFromJwk, + ok, + resolver, + verification, +} from "../util" + +export function registerIdentityTools(server: McpServer) { + server.tool( + "ack_create_controller_credential", + "Create a W3C Verifiable Credential proving that a subject DID is controlled by a controller DID.", + { + subject: z + .string() + .describe("DID of the subject (the entity being controlled)"), + controller: z + .string() + .describe("DID of the controller (the entity with authority)"), + issuer: z + .string() + .optional() + .describe("DID of the issuer. Defaults to the controller."), + }, + async ({ subject, controller, issuer }) => { + try { + const credential = createControllerCredential({ + subject: subject as DidUri, + controller: controller as DidUri, + issuer: issuer as DidUri | undefined, + }) + return ok(credential) + } catch (e) { + return err(e) + } + }, + ) + + server.tool( + "ack_sign_credential", + "Sign a W3C Verifiable Credential, returning a signed JWT string. The jwk parameter should be the JWK string returned by ack_generate_keypair.", + { + credential: z + .string() + .describe("JSON string of the W3C credential to sign"), + jwk: z + .string() + .describe( + "JWK JSON string containing the private key (from ack_generate_keypair)", + ), + did: z.string().describe("DID of the signer"), + }, + async ({ credential, jwk, did }) => { + try { + const keypair = keypairFromJwk(jwk) + const jwt = await signCredential(JSON.parse(credential), { + did: did as DidUri, + signer: createJwtSigner(keypair), + alg: curveToAlg(keypair.curve), + }) + return ok(jwt) + } catch (e) { + return err(e) + } + }, + ) + + server.tool( + "ack_verify_credential", + "Verify a signed credential JWT. Checks signature, expiration, and optionally trusted issuers.", + { + jwt: z.string().describe("The signed credential JWT string"), + trustedIssuers: z + .array(z.string()) + .optional() + .describe( + "List of trusted issuer DIDs. If provided, the credential issuer must be in this list.", + ), + }, + async ({ jwt, trustedIssuers }) => { + try { + const credential = await parseJwtCredential(jwt as JwtString, resolver) + await verifyParsedCredential(credential, { + resolver, + trustedIssuers, + }) + return verification(true, { + issuer: credential.issuer, + type: credential.type, + subject: credential.credentialSubject, + }) + } catch (e) { + return verification(false, { reason: (e as Error).message }) + } + }, + ) + + server.tool( + "ack_resolve_did", + "Resolve a DID URI to its DID Document. Supports did:key, did:web, and did:pkh methods.", + { + did: z.string().describe("The DID URI to resolve"), + }, + async ({ did }) => { + try { + return ok(await resolveDid(did, resolver)) + } catch (e) { + return err(e) + } + }, + ) +} diff --git a/tools/mcp-server/src/tools/payment-receipts.ts b/tools/mcp-server/src/tools/payment-receipts.ts new file mode 100644 index 0000000..115dfc7 --- /dev/null +++ b/tools/mcp-server/src/tools/payment-receipts.ts @@ -0,0 +1,86 @@ +/** + * ACK-Pay payment receipt tools for MCP. + */ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" +import { + createPaymentReceipt, + verifyPaymentReceipt, + type DidUri, +} from "agentcommercekit" +import { z } from "zod" + +import { err, ok, resolver, verification } from "../util" + +export function registerPaymentReceiptTools(server: McpServer) { + server.tool( + "ack_create_payment_receipt", + "Create a payment receipt as a W3C Verifiable Credential, proving that a payment was made.", + { + paymentRequestToken: z + .string() + .describe("The original payment request JWT that was fulfilled"), + paymentOptionId: z + .string() + .describe("ID of the payment option that was used"), + issuerDid: z + .string() + .describe("DID of the receipt issuer (typically the payment receiver)"), + payerDid: z.string().describe("DID of the entity that made the payment"), + metadata: z + .record(z.unknown()) + .optional() + .describe("Optional metadata about the payment"), + }, + async ({ + paymentRequestToken, + paymentOptionId, + issuerDid, + payerDid, + metadata, + }) => { + try { + const receipt = createPaymentReceipt({ + paymentRequestToken, + paymentOptionId, + issuer: issuerDid as DidUri, + payerDid: payerDid as DidUri, + metadata, + }) + return ok(receipt) + } catch (e) { + return err(e) + } + }, + ) + + server.tool( + "ack_verify_payment_receipt", + "Verify a payment receipt credential. Checks the receipt signature and optionally verifies the embedded payment request.", + { + receipt: z.string().describe("The receipt as a signed JWT string"), + trustedReceiptIssuers: z + .array(z.string()) + .optional() + .describe("Trusted receipt issuer DIDs"), + paymentRequestIssuer: z + .string() + .optional() + .describe("Expected payment request issuer DID"), + }, + async ({ receipt, trustedReceiptIssuers, paymentRequestIssuer }) => { + try { + const result = await verifyPaymentReceipt(receipt, { + resolver, + trustedReceiptIssuers, + paymentRequestIssuer, + }) + return verification(true, { + receipt: result.receipt, + paymentRequest: result.paymentRequest, + }) + } catch (e) { + return verification(false, { reason: (e as Error).message }) + } + }, + ) +} diff --git a/tools/mcp-server/src/tools/payment-requests.test.ts b/tools/mcp-server/src/tools/payment-requests.test.ts new file mode 100644 index 0000000..64a54dc --- /dev/null +++ b/tools/mcp-server/src/tools/payment-requests.test.ts @@ -0,0 +1,85 @@ +import { + createDidKeyUri, + createJwtSigner, + createSignedPaymentRequest, + generateKeypair, + verifyPaymentRequestToken, + getDidResolver, + type DidUri, + type PaymentRequestInit, +} from "agentcommercekit" +import { describe, expect, it } from "vitest" + +import { curveToAlg } from "../util" + +const resolver = getDidResolver() + +describe("payment tool operations", () => { + it("creates and verifies a payment request token", async () => { + const keypair = await generateKeypair("secp256k1") + const did = createDidKeyUri(keypair) + const signer = createJwtSigner(keypair) + + const init: PaymentRequestInit = { + id: crypto.randomUUID(), + description: "Test payment", + paymentOptions: [ + { + id: "option-1", + amount: 100, + decimals: 6, + currency: "USDC", + recipient: "0x1234567890abcdef1234567890abcdef12345678", + }, + ], + } + + const { paymentRequest, paymentRequestToken } = + await createSignedPaymentRequest(init, { + issuer: did, + signer, + algorithm: curveToAlg(keypair.curve), + }) + + expect(paymentRequest.id).toBe(init.id) + expect(paymentRequestToken).toMatch(/^eyJ/) + + // Verify the token + const { paymentRequest: verified } = await verifyPaymentRequestToken( + paymentRequestToken, + { resolver }, + ) + + expect(verified.id).toBe(init.id) + expect(verified.paymentOptions[0]!.currency).toBe("USDC") + }) + + it("rejects a payment request with wrong issuer", async () => { + const keypair = await generateKeypair("secp256k1") + const did = createDidKeyUri(keypair) + const signer = createJwtSigner(keypair) + + const { paymentRequestToken } = await createSignedPaymentRequest( + { + id: crypto.randomUUID(), + paymentOptions: [ + { + id: "opt-1", + amount: 50, + decimals: 6, + currency: "USDC", + recipient: "0xrecipient", + }, + ], + }, + { issuer: did, signer, algorithm: "ES256K" }, + ) + + await expect( + verifyPaymentRequestToken(paymentRequestToken, { + resolver, + issuer: "did:key:z6MkWrongIssuer", + }), + ).rejects.toThrow() + }) +}) diff --git a/tools/mcp-server/src/tools/payment-requests.ts b/tools/mcp-server/src/tools/payment-requests.ts new file mode 100644 index 0000000..19e7cfd --- /dev/null +++ b/tools/mcp-server/src/tools/payment-requests.ts @@ -0,0 +1,123 @@ +/** + * ACK-Pay payment request tools for MCP. + */ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" +import { + createJwtSigner, + createSignedPaymentRequest, + verifyPaymentRequestToken, + type DidUri, + type PaymentRequestInit, +} from "agentcommercekit" +import { z } from "zod" + +import { + curveToAlg, + err, + keypairFromJwk, + ok, + resolver, + verification, +} from "../util" + +const paymentOptionSchema = z.object({ + id: z.string(), + amount: z.union([z.number(), z.string()]), + decimals: z.number(), + currency: z.string(), + recipient: z.string(), + network: z.string().optional(), + paymentService: z.string().optional(), + receiptService: z.string().optional(), +}) + +export function registerPaymentRequestTools(server: McpServer) { + server.tool( + "ack_create_payment_request", + "Create a signed payment request token (JWT) for use in HTTP 402 responses. The jwk parameter should be the JWK string returned by ack_generate_keypair.", + { + description: z + .string() + .optional() + .describe("Human-readable description of what the payment is for"), + paymentOptions: z + .array(paymentOptionSchema) + .describe( + "Array of payment options (amount, currency, recipient, network)", + ), + expiresInSeconds: z + .number() + .optional() + .default(3600) + .describe("Seconds until the payment request expires"), + jwk: z + .string() + .describe( + "JWK JSON string containing the private key (from ack_generate_keypair)", + ), + did: z.string().describe("DID of the payment requester"), + }, + async ({ description, paymentOptions, expiresInSeconds, jwk, did }) => { + try { + if (paymentOptions.length === 0) { + throw new Error("At least one payment option is required") + } + + const keypair = keypairFromJwk(jwk) + + const init: PaymentRequestInit = { + id: crypto.randomUUID(), + description, + paymentOptions: paymentOptions as [ + (typeof paymentOptions)[0], + ...typeof paymentOptions, + ], + } + + if (expiresInSeconds) { + init.expiresAt = new Date( + Date.now() + expiresInSeconds * 1000, + ).toISOString() + } + + const result = await createSignedPaymentRequest(init, { + issuer: did as DidUri, + signer: createJwtSigner(keypair), + algorithm: curveToAlg(keypair.curve), + }) + + return ok(result) + } catch (e) { + return err(e) + } + }, + ) + + server.tool( + "ack_verify_payment_request", + "Verify and parse a payment request JWT. Returns the decoded payment request if valid.", + { + token: z.string().describe("The payment request JWT string"), + issuer: z + .string() + .optional() + .describe( + "Expected issuer DID. If provided, verifies the token was issued by this DID.", + ), + }, + async ({ token, issuer }) => { + try { + const { paymentRequest, parsed } = await verifyPaymentRequestToken( + token, + { resolver, issuer }, + ) + return verification(true, { + paymentRequest, + issuer: parsed.issuer, + }) + } catch (e) { + return verification(false, { reason: (e as Error).message }) + } + }, + ) +} diff --git a/tools/mcp-server/src/tools/utility.ts b/tools/mcp-server/src/tools/utility.ts new file mode 100644 index 0000000..5cc3f49 --- /dev/null +++ b/tools/mcp-server/src/tools/utility.ts @@ -0,0 +1,40 @@ +/** + * Utility tools for MCP. + */ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" +import { + bytesToHexString, + createDidKeyUri, + generateKeypair, + keypairToJwk, +} from "agentcommercekit" +import { z } from "zod" + +import { err, ok } from "../util" + +export function registerUtilityTools(server: McpServer) { + server.tool( + "ack_generate_keypair", + "Generate a new cryptographic keypair. Returns the private key (hex), public key (hex), JWK, DID, and curve. Use the JWK value when calling other tools that require signing.", + { + curve: z + .enum(["secp256k1", "secp256r1", "Ed25519"]) + .default("secp256k1") + .describe("Cryptographic curve to use"), + }, + async ({ curve }) => { + try { + const keypair = await generateKeypair(curve) + return ok({ + curve, + did: createDidKeyUri(keypair), + jwk: JSON.stringify(keypairToJwk(keypair)), + privateKey: bytesToHexString(keypair.privateKey), + publicKey: bytesToHexString(keypair.publicKey), + }) + } catch (e) { + return err(e) + } + }, + ) +} diff --git a/tools/mcp-server/src/util.test.ts b/tools/mcp-server/src/util.test.ts new file mode 100644 index 0000000..6c99579 --- /dev/null +++ b/tools/mcp-server/src/util.test.ts @@ -0,0 +1,97 @@ +import { generateKeypair, keypairToJwk } from "agentcommercekit" +import { describe, expect, it } from "vitest" + +import { curveToAlg, err, keypairFromJwk, ok, verification } from "./util" + +describe("keypairFromJwk", () => { + it("reconstructs a secp256k1 keypair from its JWK", async () => { + const original = await generateKeypair("secp256k1") + const jwk = keypairToJwk(original) + const restored = keypairFromJwk(JSON.stringify(jwk)) + + expect(restored.curve).toBe("secp256k1") + expect(Buffer.from(restored.privateKey).toString("hex")).toBe( + Buffer.from(original.privateKey).toString("hex"), + ) + expect(Buffer.from(restored.publicKey).toString("hex")).toBe( + Buffer.from(original.publicKey).toString("hex"), + ) + }) + + it("reconstructs an Ed25519 keypair from its JWK", async () => { + const original = await generateKeypair("Ed25519") + const jwk = keypairToJwk(original) + const restored = keypairFromJwk(JSON.stringify(jwk)) + + expect(restored.curve).toBe("Ed25519") + expect(Buffer.from(restored.privateKey).toString("hex")).toBe( + Buffer.from(original.privateKey).toString("hex"), + ) + }) + + it("throws on invalid JSON", () => { + expect(() => keypairFromJwk("not json")).toThrow() + }) + + it("throws on JWK missing required fields", () => { + expect(() => keypairFromJwk(JSON.stringify({ kty: "EC" }))).toThrow() + }) +}) + +describe("curveToAlg", () => { + it("maps secp256k1 to ES256K", () => { + expect(curveToAlg("secp256k1")).toBe("ES256K") + }) + + it("maps secp256r1 to ES256", () => { + expect(curveToAlg("secp256r1")).toBe("ES256") + }) + + it("maps Ed25519 to EdDSA", () => { + expect(curveToAlg("Ed25519")).toBe("EdDSA") + }) + + it("throws on unsupported curve", () => { + expect(() => curveToAlg("P-384")).toThrow("Unsupported curve") + }) +}) + +describe("response helpers", () => { + it("ok wraps data in MCP content format", () => { + const result = ok({ foo: "bar" }) + expect(result.isError).toBeUndefined() + expect(result.content[0]!.type).toBe("text") + expect(JSON.parse((result.content[0] as { text: string }).text)).toEqual({ + foo: "bar", + }) + }) + + it("ok passes strings through without double-encoding", () => { + const result = ok("eyJhbGciOiJFUzI1NksifQ.test.sig") + expect((result.content[0] as { text: string }).text).toBe( + "eyJhbGciOiJFUzI1NksifQ.test.sig", + ) + }) + + it("err wraps error message with isError flag", () => { + const result = err(new Error("something broke")) + expect(result.isError).toBe(true) + expect((result.content[0] as { text: string }).text).toBe( + "Error: something broke", + ) + }) + + it("verification returns valid/invalid with data", () => { + const valid = verification(true, { score: 82 }) + expect(JSON.parse((valid.content[0] as { text: string }).text)).toEqual({ + valid: true, + score: 82, + }) + + const invalid = verification(false, { reason: "expired" }) + expect(JSON.parse((invalid.content[0] as { text: string }).text)).toEqual({ + valid: false, + reason: "expired", + }) + }) +}) diff --git a/tools/mcp-server/src/util.ts b/tools/mcp-server/src/util.ts new file mode 100644 index 0000000..9cb50ef --- /dev/null +++ b/tools/mcp-server/src/util.ts @@ -0,0 +1,69 @@ +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js" +/** + * Shared utilities for MCP tools. + */ +import { + getDidResolver, + jwkToKeypair, + type JwtAlgorithm, + type Keypair, +} from "agentcommercekit" + +/** Shared DID resolver instance. */ +export const resolver = getDidResolver() + +/** + * Reconstruct a Keypair from a JWK JSON string. + * + * Using JWK instead of raw hex + curve avoids the risk of curve mismatch — + * the JWK's `crv` field is always bundled with the key material. + */ +export function keypairFromJwk(jwkJson: string): Keypair { + const jwk = JSON.parse(jwkJson) + return jwkToKeypair(jwk) +} + +/** Map a curve name to its JWT algorithm identifier. */ +export function curveToAlg(curve: string): JwtAlgorithm { + switch (curve) { + case "secp256k1": + return "ES256K" + case "secp256r1": + return "ES256" + case "Ed25519": + return "EdDSA" + default: + throw new Error( + `Unsupported curve: ${curve}. Use secp256k1, secp256r1, or Ed25519.`, + ) + } +} + +/** Return a successful MCP tool result with a JSON text response. */ +export function ok(data: unknown): CallToolResult { + return { + content: [ + { + type: "text", + text: typeof data === "string" ? data : JSON.stringify(data, null, 2), + }, + ], + } +} + +/** Return an error MCP tool result. */ +export function err(error: unknown): CallToolResult { + const message = error instanceof Error ? error.message : String(error) + return { + content: [{ type: "text", text: `Error: ${message}` }], + isError: true, + } +} + +/** Return a verification result (valid or invalid, never an error). */ +export function verification( + valid: boolean, + data: Record, +): CallToolResult { + return ok({ valid, ...data }) +} diff --git a/tools/mcp-server/tsconfig.json b/tools/mcp-server/tsconfig.json new file mode 100644 index 0000000..6273d27 --- /dev/null +++ b/tools/mcp-server/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@repo/typescript-config/base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/tools/mcp-server/vitest.config.ts b/tools/mcp-server/vitest.config.ts new file mode 100644 index 0000000..f8e3275 --- /dev/null +++ b/tools/mcp-server/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config" + +export default defineConfig({ + test: { + watch: false, + }, +}) From e118c264db3b238584b68a75ef8cb1c550061922 Mon Sep 17 00:00:00 2001 From: ak68a Date: Wed, 25 Mar 2026 22:01:34 -0500 Subject: [PATCH 2/7] test: add end-to-end workflow eval for MCP tools --- tools/mcp-server/src/tools/workflow.test.ts | 189 ++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 tools/mcp-server/src/tools/workflow.test.ts diff --git a/tools/mcp-server/src/tools/workflow.test.ts b/tools/mcp-server/src/tools/workflow.test.ts new file mode 100644 index 0000000..705195a --- /dev/null +++ b/tools/mcp-server/src/tools/workflow.test.ts @@ -0,0 +1,189 @@ +/** + * End-to-end workflow eval for MCP tools. + * + * Simulates what an AI agent would do: generate a keypair, create + * credentials, sign them, verify them, issue a payment request, + * verify it, create a receipt, and verify the receipt. If any step + * produces invalid output, the whole workflow fails. + */ +import { + createControllerCredential, + createDidKeyUri, + createJwtSigner, + createPaymentReceipt, + createSignedPaymentRequest, + generateKeypair, + getDidResolver, + keypairToJwk, + parseJwtCredential, + signCredential, + verifyParsedCredential, + verifyPaymentRequestToken, + verifyPaymentReceipt, + type DidUri, + type JwtString, + type PaymentRequestInit, +} from "agentcommercekit" +import { describe, expect, it } from "vitest" + +import { curveToAlg } from "../util" + +const resolver = getDidResolver() + +describe("full agent workflow", () => { + it("completes an identity + payment cycle end-to-end", async () => { + // 1. Generate keypairs for owner and agent + const ownerKeypair = await generateKeypair("secp256k1") + const agentKeypair = await generateKeypair("secp256k1") + const ownerDid = createDidKeyUri(ownerKeypair) + const agentDid = createDidKeyUri(agentKeypair) + + // Verify JWK round-trip works (this is how MCP tools pass keys) + const ownerJwk = keypairToJwk(ownerKeypair) + expect(ownerJwk.crv).toBe("secp256k1") + + // 2. Owner creates a controller credential for the agent + const credential = createControllerCredential({ + subject: agentDid, + controller: ownerDid, + }) + + expect(credential.type).toContain("ControllerCredential") + expect(credential.credentialSubject.id).toBe(agentDid) + + // 3. Owner signs the credential + const signedCredential = await signCredential(credential, { + did: ownerDid, + signer: createJwtSigner(ownerKeypair), + alg: curveToAlg(ownerKeypair.curve), + }) + + expect(signedCredential).toMatch(/^eyJ/) + + // 4. Anyone can verify the signed credential + const parsed = await parseJwtCredential(signedCredential, resolver) + await verifyParsedCredential(parsed, { resolver }) + + expect(parsed.issuer).toEqual({ id: ownerDid }) + expect(parsed.credentialSubject.controller).toBe(ownerDid) + + // 5. Agent creates a payment request + const paymentInit: PaymentRequestInit = { + id: crypto.randomUUID(), + description: "API access fee", + paymentOptions: [ + { + id: "option-eth", + amount: "0.001", + decimals: 18, + currency: "ETH", + recipient: "0x1234567890abcdef1234567890abcdef12345678", + network: "eip155:11155111", + }, + ], + expiresAt: new Date(Date.now() + 3600_000).toISOString(), + } + + const { paymentRequest, paymentRequestToken } = + await createSignedPaymentRequest(paymentInit, { + issuer: agentDid, + signer: createJwtSigner(agentKeypair), + algorithm: curveToAlg(agentKeypair.curve), + }) + + expect(paymentRequest.id).toBe(paymentInit.id) + expect(paymentRequestToken).toMatch(/^eyJ/) + + // 6. Verify the payment request token + const { paymentRequest: verifiedRequest } = await verifyPaymentRequestToken( + paymentRequestToken, + { resolver }, + ) + + expect(verifiedRequest.id).toBe(paymentInit.id) + expect(verifiedRequest.paymentOptions[0]!.currency).toBe("ETH") + + // 7. After payment, agent issues a receipt + const receipt = createPaymentReceipt({ + paymentRequestToken, + paymentOptionId: "option-eth", + issuer: agentDid, + payerDid: ownerDid, + metadata: { txHash: "0xabc123" }, + }) + + expect(receipt.type).toContain("PaymentReceiptCredential") + + // 8. Sign and verify the receipt + const signedReceipt = await signCredential(receipt, { + did: agentDid, + signer: createJwtSigner(agentKeypair), + alg: curveToAlg(agentKeypair.curve), + }) + + const { receipt: verifiedReceipt } = await verifyPaymentReceipt( + signedReceipt, + { resolver }, + ) + + expect(verifiedReceipt.issuer).toEqual({ id: agentDid }) + }) + + it("rejects a credential signed by the wrong key", async () => { + const ownerKeypair = await generateKeypair("secp256k1") + const attackerKeypair = await generateKeypair("secp256k1") + const ownerDid = createDidKeyUri(ownerKeypair) + const agentDid = createDidKeyUri(await generateKeypair("secp256k1")) + + const credential = createControllerCredential({ + subject: agentDid, + controller: ownerDid, + }) + + // Attacker signs with their own key but claims to be the owner. + // The DID resolver will find the owner's public key, which won't + // match the attacker's signature — rejection happens at parse time. + const signedByAttacker = await signCredential(credential, { + did: ownerDid, + signer: createJwtSigner(attackerKeypair), + alg: "ES256K", + }) + + await expect( + parseJwtCredential(signedByAttacker as JwtString, resolver), + ).rejects.toThrow() + }) + + it("rejects a payment request from an untrusted issuer", async () => { + const keypair = await generateKeypair("secp256k1") + const did = createDidKeyUri(keypair) + + const { paymentRequestToken } = await createSignedPaymentRequest( + { + id: crypto.randomUUID(), + paymentOptions: [ + { + id: "opt-1", + amount: 100, + decimals: 6, + currency: "USDC", + recipient: "0xrecipient", + }, + ], + }, + { + issuer: did, + signer: createJwtSigner(keypair), + algorithm: "ES256K", + }, + ) + + // Valid token, but issuer doesn't match expected + await expect( + verifyPaymentRequestToken(paymentRequestToken, { + resolver, + issuer: "did:key:z6MkWrongIssuer", + }), + ).rejects.toThrow() + }) +}) From f8629930dae3235ef02508ab603c46b0523dc4b4 Mon Sep 17 00:00:00 2001 From: ak68a Date: Wed, 25 Mar 2026 22:07:01 -0500 Subject: [PATCH 3/7] docs: add missing docstrings to satisfy coverage check --- tools/mcp-server/src/tools/identity.ts | 1 + tools/mcp-server/src/tools/payment-receipts.ts | 1 + tools/mcp-server/src/tools/payment-requests.ts | 1 + tools/mcp-server/src/tools/utility.ts | 1 + 4 files changed, 4 insertions(+) diff --git a/tools/mcp-server/src/tools/identity.ts b/tools/mcp-server/src/tools/identity.ts index 97722e8..0da3fa1 100644 --- a/tools/mcp-server/src/tools/identity.ts +++ b/tools/mcp-server/src/tools/identity.ts @@ -23,6 +23,7 @@ import { verification, } from "../util" +/** Register ACK-ID identity tools on the MCP server. */ export function registerIdentityTools(server: McpServer) { server.tool( "ack_create_controller_credential", diff --git a/tools/mcp-server/src/tools/payment-receipts.ts b/tools/mcp-server/src/tools/payment-receipts.ts index 115dfc7..6a72511 100644 --- a/tools/mcp-server/src/tools/payment-receipts.ts +++ b/tools/mcp-server/src/tools/payment-receipts.ts @@ -11,6 +11,7 @@ import { z } from "zod" import { err, ok, resolver, verification } from "../util" +/** Register ACK-Pay payment receipt tools on the MCP server. */ export function registerPaymentReceiptTools(server: McpServer) { server.tool( "ack_create_payment_receipt", diff --git a/tools/mcp-server/src/tools/payment-requests.ts b/tools/mcp-server/src/tools/payment-requests.ts index 19e7cfd..2db64b9 100644 --- a/tools/mcp-server/src/tools/payment-requests.ts +++ b/tools/mcp-server/src/tools/payment-requests.ts @@ -31,6 +31,7 @@ const paymentOptionSchema = z.object({ receiptService: z.string().optional(), }) +/** Register ACK-Pay payment request tools on the MCP server. */ export function registerPaymentRequestTools(server: McpServer) { server.tool( "ack_create_payment_request", diff --git a/tools/mcp-server/src/tools/utility.ts b/tools/mcp-server/src/tools/utility.ts index 5cc3f49..ce0ac26 100644 --- a/tools/mcp-server/src/tools/utility.ts +++ b/tools/mcp-server/src/tools/utility.ts @@ -12,6 +12,7 @@ import { z } from "zod" import { err, ok } from "../util" +/** Register utility tools (keypair generation) on the MCP server. */ export function registerUtilityTools(server: McpServer) { server.tool( "ack_generate_keypair", From d0a9ed6003bde9747dfc7752eb3f969075702587 Mon Sep 17 00:00:00 2001 From: ak68a Date: Thu, 26 Mar 2026 16:53:01 -0500 Subject: [PATCH 4/7] style: rename test titles to assertive verb pattern per project conventions --- tools/mcp-server/src/tools/workflow.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/mcp-server/src/tools/workflow.test.ts b/tools/mcp-server/src/tools/workflow.test.ts index 705195a..6eb3221 100644 --- a/tools/mcp-server/src/tools/workflow.test.ts +++ b/tools/mcp-server/src/tools/workflow.test.ts @@ -31,7 +31,7 @@ import { curveToAlg } from "../util" const resolver = getDidResolver() describe("full agent workflow", () => { - it("completes an identity + payment cycle end-to-end", async () => { + it("creates and verifies an identity + payment cycle end-to-end", async () => { // 1. Generate keypairs for owner and agent const ownerKeypair = await generateKeypair("secp256k1") const agentKeypair = await generateKeypair("secp256k1") @@ -129,7 +129,7 @@ describe("full agent workflow", () => { expect(verifiedReceipt.issuer).toEqual({ id: agentDid }) }) - it("rejects a credential signed by the wrong key", async () => { + it("throws for a credential signed by the wrong key", async () => { const ownerKeypair = await generateKeypair("secp256k1") const attackerKeypair = await generateKeypair("secp256k1") const ownerDid = createDidKeyUri(ownerKeypair) @@ -154,7 +154,7 @@ describe("full agent workflow", () => { ).rejects.toThrow() }) - it("rejects a payment request from an untrusted issuer", async () => { + it("throws for a payment request from an untrusted issuer", async () => { const keypair = await generateKeypair("secp256k1") const did = createDidKeyUri(keypair) From 684da0b252416a5720bdfbd6b6b960347a0384fd Mon Sep 17 00:00:00 2001 From: ak68a Date: Thu, 26 Mar 2026 17:21:42 -0500 Subject: [PATCH 5/7] fix(mcp-server): make binary executable and fix expiry edge case - Change shebang from node to npx tsx so TypeScript source runs directly - Fix expiresInSeconds check: use !== undefined instead of truthy check so that 0 correctly creates an immediately-expiring request --- tools/mcp-server/src/index.ts | 2 +- tools/mcp-server/src/tools/payment-requests.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) mode change 100644 => 100755 tools/mcp-server/src/index.ts diff --git a/tools/mcp-server/src/index.ts b/tools/mcp-server/src/index.ts old mode 100644 new mode 100755 index a2a073c..78e3244 --- a/tools/mcp-server/src/index.ts +++ b/tools/mcp-server/src/index.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env node +#!/usr/bin/env -S npx tsx /** * ACK MCP Server * diff --git a/tools/mcp-server/src/tools/payment-requests.ts b/tools/mcp-server/src/tools/payment-requests.ts index 2db64b9..56d6373 100644 --- a/tools/mcp-server/src/tools/payment-requests.ts +++ b/tools/mcp-server/src/tools/payment-requests.ts @@ -75,7 +75,7 @@ export function registerPaymentRequestTools(server: McpServer) { ], } - if (expiresInSeconds) { + if (expiresInSeconds !== undefined) { init.expiresAt = new Date( Date.now() + expiresInSeconds * 1000, ).toISOString() From 22c526e129922dd4e8c003480c89b9b6801d039c Mon Sep 17 00:00:00 2001 From: ak68a Date: Thu, 26 Mar 2026 17:22:58 -0500 Subject: [PATCH 6/7] fix(mcp-server): harden error handling and expiry schema - Use instanceof Error check instead of unsafe (e as Error).message cast in all 3 verification catch blocks (identity, payment-requests, payment-receipts) - Remove .default(3600) from expiresInSeconds schema so omitting the field leaves expiresAt unset rather than always setting it - Add .int().nonnegative() validation to expiresInSeconds --- tools/mcp-server/src/tools/identity.ts | 3 ++- tools/mcp-server/src/tools/payment-receipts.ts | 3 ++- tools/mcp-server/src/tools/payment-requests.ts | 6 ++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/tools/mcp-server/src/tools/identity.ts b/tools/mcp-server/src/tools/identity.ts index 0da3fa1..efff033 100644 --- a/tools/mcp-server/src/tools/identity.ts +++ b/tools/mcp-server/src/tools/identity.ts @@ -108,7 +108,8 @@ export function registerIdentityTools(server: McpServer) { subject: credential.credentialSubject, }) } catch (e) { - return verification(false, { reason: (e as Error).message }) + const reason = e instanceof Error ? e.message : String(e) + return verification(false, { reason }) } }, ) diff --git a/tools/mcp-server/src/tools/payment-receipts.ts b/tools/mcp-server/src/tools/payment-receipts.ts index 6a72511..1dc2a9d 100644 --- a/tools/mcp-server/src/tools/payment-receipts.ts +++ b/tools/mcp-server/src/tools/payment-receipts.ts @@ -80,7 +80,8 @@ export function registerPaymentReceiptTools(server: McpServer) { paymentRequest: result.paymentRequest, }) } catch (e) { - return verification(false, { reason: (e as Error).message }) + const reason = e instanceof Error ? e.message : String(e) + return verification(false, { reason }) } }, ) diff --git a/tools/mcp-server/src/tools/payment-requests.ts b/tools/mcp-server/src/tools/payment-requests.ts index 56d6373..b4421bf 100644 --- a/tools/mcp-server/src/tools/payment-requests.ts +++ b/tools/mcp-server/src/tools/payment-requests.ts @@ -48,8 +48,9 @@ export function registerPaymentRequestTools(server: McpServer) { ), expiresInSeconds: z .number() + .int() + .nonnegative() .optional() - .default(3600) .describe("Seconds until the payment request expires"), jwk: z .string() @@ -117,7 +118,8 @@ export function registerPaymentRequestTools(server: McpServer) { issuer: parsed.issuer, }) } catch (e) { - return verification(false, { reason: (e as Error).message }) + const reason = e instanceof Error ? e.message : String(e) + return verification(false, { reason }) } }, ) From a1ce82d4de70027858df479145969dc565390ab2 Mon Sep 17 00:00:00 2001 From: ak68a Date: Thu, 26 Mar 2026 19:14:06 -0500 Subject: [PATCH 7/7] fix(mcp-server): validate JSON.parse result before passing to signCredential --- tools/mcp-server/src/tools/identity.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/mcp-server/src/tools/identity.ts b/tools/mcp-server/src/tools/identity.ts index efff033..293fcbe 100644 --- a/tools/mcp-server/src/tools/identity.ts +++ b/tools/mcp-server/src/tools/identity.ts @@ -71,7 +71,11 @@ export function registerIdentityTools(server: McpServer) { async ({ credential, jwk, did }) => { try { const keypair = keypairFromJwk(jwk) - const jwt = await signCredential(JSON.parse(credential), { + const parsed = JSON.parse(credential) + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { + throw new Error("credential must be a JSON object") + } + const jwt = await signCredential(parsed, { did: did as DidUri, signer: createJwtSigner(keypair), alg: curveToAlg(keypair.curve),