Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
282 changes: 282 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions tools/mcp-server/package.json
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:*"
}
}
28 changes: 28 additions & 0 deletions tools/mcp-server/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env -S npx tsx
/**
* 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)
71 changes: 71 additions & 0 deletions tools/mcp-server/src/tools/identity.test.ts
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/)
})
})
135 changes: 135 additions & 0 deletions tools/mcp-server/src/tools/identity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/**
* 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"

/** Register ACK-ID identity tools on the MCP server. */
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 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),
})
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) {
const reason = e instanceof Error ? e.message : String(e)
return verification(false, { reason })
}
Comment on lines +114 to +117
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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 f

Repository: 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 -30

Repository: 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-Error throws.

At line 111, (e as Error).message returns undefined when a non-Error value is thrown (e.g., throw "message" or throw { code: 123 }), which weakens failure diagnostics. This pattern appears in 3 locations across the codebase.

🔧 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:

  • tools/mcp-server/src/tools/identity.ts:111
  • tools/mcp-server/src/tools/payment-requests.ts:120
  • tools/mcp-server/src/tools/payment-receipts.ts:83
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} 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 })
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/mcp-server/src/tools/identity.ts` around lines 110 - 112, The catch
blocks that return verification(false, { reason: (e as Error).message }) can
yield undefined for non-Error throws; replace the cast with a safe message
extractor (e.g., const reason = e instanceof Error ? e.message : (typeof e ===
'string' ? e : JSON.stringify(e) || String(e))) and return verification(false, {
reason }); apply this change to the identical patterns in identity.ts (the
verification(...) call), payment-requests.ts, and payment-receipts.ts so all
non-Error throws produce a stable, informative reason string.

},
)

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)
}
},
)
}
88 changes: 88 additions & 0 deletions tools/mcp-server/src/tools/payment-receipts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* 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"

/** Register ACK-Pay payment receipt tools on the MCP server. */
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) {
const reason = e instanceof Error ? e.message : String(e)
return verification(false, { reason })
}
},
)
}
Loading