Skip to content
Draft
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)
159 changes: 159 additions & 0 deletions tools/mcp-server/src/server.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/**
* MCP transport-level smoke tests.
*
* Spawns the actual server over stdio, performs the MCP handshake,
* and exercises tools through the protocol — the same path a real
* MCP client (Claude, Cursor, etc.) would take.
*/
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
import { afterAll, beforeAll, describe, expect, it } from "vitest"

const EXPECTED_TOOLS = [
"ack_create_controller_credential",
"ack_sign_credential",
"ack_verify_credential",
"ack_resolve_did",
"ack_create_payment_request",
"ack_verify_payment_request",
"ack_create_payment_receipt",
"ack_verify_payment_receipt",
"ack_generate_keypair",
]

describe("MCP server over stdio", () => {
let client: Client
let transport: StdioClientTransport

beforeAll(async () => {
transport = new StdioClientTransport({
command: "tsx",
args: ["./src/index.ts"],
cwd: import.meta.dirname + "/..",
stderr: "pipe",
})

client = new Client({ name: "test-client", version: "0.0.1" })
await client.connect(transport)
}, 15_000)

afterAll(async () => {
await client.close()
})

it("completes the MCP handshake", () => {
const info = client.getServerVersion()
expect(info).toBeDefined()
expect(info!.name).toBe("ack")
})

it("lists all 9 tools", async () => {
const { tools } = await client.listTools()
const names = tools.map((t) => t.name).sort()
expect(names).toEqual([...EXPECTED_TOOLS].sort())
})

it("generates a keypair via ack_generate_keypair", async () => {
const result = await client.callTool({
name: "ack_generate_keypair",
arguments: { curve: "Ed25519" },
})

expect(result.isError).toBeFalsy()

const text = (result.content as Array<{ type: string; text: string }>)[0]!
.text
const parsed = JSON.parse(text)

expect(parsed.curve).toBe("Ed25519")
expect(parsed.did).toMatch(/^did:key:z6Mk/)
expect(parsed.jwk).toBeDefined()
})

it("resolves a did:key via ack_resolve_did", async () => {
// Generate a key first, then resolve its DID
const genResult = await client.callTool({
name: "ack_generate_keypair",
arguments: { curve: "secp256k1" },
})

const genText = (
genResult.content as Array<{ type: string; text: string }>
)[0]!.text
const { did } = JSON.parse(genText)

const resolveResult = await client.callTool({
name: "ack_resolve_did",
arguments: { did },
})

expect(resolveResult.isError).toBeFalsy()

const resolveText = (
resolveResult.content as Array<{ type: string; text: string }>
)[0]!.text
const doc = JSON.parse(resolveText)

expect(doc.did).toBe(did)
expect(doc.didDocument).toBeDefined()
expect(doc.didDocument.verificationMethod).toBeDefined()
})

it("round-trips sign and verify through the protocol", async () => {
// Generate owner + agent keypairs
const ownerResult = await client.callTool({
name: "ack_generate_keypair",
arguments: { curve: "secp256k1" },
})
const owner = JSON.parse(
(ownerResult.content as Array<{ type: string; text: string }>)[0]!.text,
)

const agentResult = await client.callTool({
name: "ack_generate_keypair",
arguments: { curve: "secp256k1" },
})
const agent = JSON.parse(
(agentResult.content as Array<{ type: string; text: string }>)[0]!.text,
)

// Create a controller credential
const credResult = await client.callTool({
name: "ack_create_controller_credential",
arguments: {
subjectDid: agent.did,
controllerDid: owner.did,
},
})
expect(credResult.isError).toBeFalsy()
const credential = (
credResult.content as Array<{ type: string; text: string }>
)[0]!.text

// Sign it
const signResult = await client.callTool({
name: "ack_sign_credential",
arguments: {
credential,
signerJwk: owner.jwk,
signerDid: owner.did,
},
})
expect(signResult.isError).toBeFalsy()
const jwt = (
signResult.content as Array<{ type: string; text: string }>
)[0]!.text
expect(jwt).toMatch(/^eyJ/)

// Verify it
const verifyResult = await client.callTool({
name: "ack_verify_credential",
arguments: { jwt },
})
expect(verifyResult.isError).toBeFalsy()
const verification = JSON.parse(
(verifyResult.content as Array<{ type: string; text: string }>)[0]!.text,
)
expect(verification.valid).toBe(true)
})
})
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/)
})
})
Loading