-
Notifications
You must be signed in to change notification settings - Fork 100
feat: add MCP server for ACK-ID and ACK-Pay operations #72
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
aadc0a9
e118c26
f862993
d0a9ed6
684da0b
22c526e
a1ce82d
d41df0f
bb92222
f7fc295
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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:*" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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/) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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 }) | ||||||||||||||||
| } | ||||||||||||||||
|
Comment on lines
+114
to
+117
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Read-only check: find catch blocks that assume Error shape via `(e as Error).message`
rg -nP --type ts '\(e as Error\)\.message'Repository: agentcommercekit/ack Length of output: 414 🏁 Script executed: cd tools/mcp-server && find . -name "identity.ts" -type fRepository: agentcommercekit/ack Length of output: 87 🏁 Script executed: cat -n tools/mcp-server/src/tools/identity.ts | sed -n '100,120p'Repository: agentcommercekit/ack Length of output: 869 🏁 Script executed: # Check the verification function signature and its usage
rg -A 3 -B 3 "const verification" tools/mcp-server/src/tools/Repository: agentcommercekit/ack Length of output: 46 🏁 Script executed: cat -n tools/mcp-server/src/tools/identity.ts | head -30Repository: agentcommercekit/ack Length of output: 985 🏁 Script executed: # Search for verification function definition
rg -n "function verification|const verification|export.*verification" tools/mcp-server/src/Repository: agentcommercekit/ack Length of output: 125 🏁 Script executed: cat -n tools/mcp-server/src/util.ts | sed -n '64,85p'Repository: agentcommercekit/ack Length of output: 240 🏁 Script executed: # Also check the other two files where this pattern appears
cat -n tools/mcp-server/src/tools/payment-requests.ts | sed -n '115,125p'Repository: agentcommercekit/ack Length of output: 349 🏁 Script executed: cat -n tools/mcp-server/src/tools/payment-receipts.ts | sed -n '78,88p'Repository: agentcommercekit/ack Length of output: 374 Harden error handling for non- At line 111, 🔧 Proposed fix- } catch (e) {
- return verification(false, { reason: (e as Error).message })
+ } catch (e) {
+ const reason = e instanceof Error ? e.message : String(e)
+ return verification(false, { reason })
}Apply this fix to:
📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
| }, | ||||||||||||||||
| ) | ||||||||||||||||
|
|
||||||||||||||||
| 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) | ||||||||||||||||
| } | ||||||||||||||||
| }, | ||||||||||||||||
| ) | ||||||||||||||||
| } | ||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 }) | ||
| } | ||
| }, | ||
| ) | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.