@a3stack/data 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +34 -0
- package/src/client.ts +189 -0
- package/src/index.ts +10 -0
- package/src/probe.ts +153 -0
- package/src/server.ts +336 -0
- package/src/types.ts +82 -0
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@a3stack/data",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server/client helpers with built-in identity verification and payment gating",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --outDir dist",
|
|
18
|
+
"typecheck": "tsc --noEmit"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@a3stack/identity": "file:../identity",
|
|
22
|
+
"@a3stack/payments": "file:../payments",
|
|
23
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
24
|
+
"viem": "^2.39.3",
|
|
25
|
+
"zod": "^3.24.2"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"tsup": "^8.0.0",
|
|
29
|
+
"typescript": "^5.4.0"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18.0.0"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createAgentMcpClient — MCP client with identity resolution + payment support
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
6
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
7
|
+
import { getMcpEndpoint, verifyAgent } from "@a3stack/identity";
|
|
8
|
+
import { PaymentClient } from "@a3stack/payments";
|
|
9
|
+
import type { AgentMcpClientConfig } from "./types.js";
|
|
10
|
+
|
|
11
|
+
type ToolResult = {
|
|
12
|
+
content: Array<{ type: string; text?: string; data?: unknown }>;
|
|
13
|
+
isError?: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type ResourceContent = {
|
|
17
|
+
uri: string;
|
|
18
|
+
text?: string;
|
|
19
|
+
blob?: string;
|
|
20
|
+
mimeType?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A connected MCP client with optional identity verification + payment
|
|
25
|
+
*/
|
|
26
|
+
export class AgentMcpClientInstance {
|
|
27
|
+
private client: Client;
|
|
28
|
+
private transport: StreamableHTTPClientTransport;
|
|
29
|
+
private config: AgentMcpClientConfig;
|
|
30
|
+
private payer?: PaymentClient;
|
|
31
|
+
private _agentVerification?: Awaited<ReturnType<typeof verifyAgent>>;
|
|
32
|
+
private _url: URL;
|
|
33
|
+
|
|
34
|
+
constructor(
|
|
35
|
+
client: Client,
|
|
36
|
+
transport: StreamableHTTPClientTransport,
|
|
37
|
+
config: AgentMcpClientConfig,
|
|
38
|
+
url: URL,
|
|
39
|
+
payer?: PaymentClient,
|
|
40
|
+
verification?: Awaited<ReturnType<typeof verifyAgent>>
|
|
41
|
+
) {
|
|
42
|
+
this.client = client;
|
|
43
|
+
this.transport = transport;
|
|
44
|
+
this.config = config;
|
|
45
|
+
this._url = url;
|
|
46
|
+
this.payer = payer;
|
|
47
|
+
this._agentVerification = verification;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* List available tools from the server
|
|
52
|
+
*/
|
|
53
|
+
async listTools(): Promise<
|
|
54
|
+
Array<{ name: string; description?: string; inputSchema?: unknown }>
|
|
55
|
+
> {
|
|
56
|
+
const result = await this.client.listTools();
|
|
57
|
+
return result.tools;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Call a tool on the server
|
|
62
|
+
*/
|
|
63
|
+
async callTool(name: string, args?: Record<string, unknown>): Promise<ToolResult> {
|
|
64
|
+
return this.client.callTool({ name, arguments: args ?? {} }) as Promise<ToolResult>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* List available resources from the server
|
|
69
|
+
*/
|
|
70
|
+
async listResources(): Promise<Array<{ uri: string; name?: string; description?: string }>> {
|
|
71
|
+
const result = await this.client.listResources();
|
|
72
|
+
return result.resources;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Read a resource at a given URI
|
|
77
|
+
*/
|
|
78
|
+
async readResource(uri: string): Promise<ResourceContent[]> {
|
|
79
|
+
const result = await this.client.readResource({ uri });
|
|
80
|
+
return result.contents as ResourceContent[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Read the agent's identity resource (shorthand)
|
|
85
|
+
*/
|
|
86
|
+
async getAgentIdentity(): Promise<Record<string, unknown> | null> {
|
|
87
|
+
try {
|
|
88
|
+
const contents = await this.readResource("agent://identity");
|
|
89
|
+
if (!contents.length || !contents[0].text) return null;
|
|
90
|
+
return JSON.parse(contents[0].text);
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get the verification result for this agent (if connected by global ID)
|
|
98
|
+
*/
|
|
99
|
+
get verification() {
|
|
100
|
+
return this._agentVerification ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get the connected server URL
|
|
105
|
+
*/
|
|
106
|
+
get url(): URL {
|
|
107
|
+
return this._url;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Close the connection
|
|
112
|
+
*/
|
|
113
|
+
async close(): Promise<void> {
|
|
114
|
+
await this.client.close();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Connect to an MCP server by ERC-8004 global ID or direct URL.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* // By identity
|
|
123
|
+
* const client = await createAgentMcpClient({
|
|
124
|
+
* agentId: "eip155:8453:0x8004...#2376",
|
|
125
|
+
* payer: { account },
|
|
126
|
+
* });
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* // By URL
|
|
130
|
+
* const client = await createAgentMcpClient({ url: "https://mcp.agent.eth/mcp" });
|
|
131
|
+
*/
|
|
132
|
+
export async function createAgentMcpClient(
|
|
133
|
+
config: AgentMcpClientConfig
|
|
134
|
+
): Promise<AgentMcpClientInstance> {
|
|
135
|
+
if (!config.agentId && !config.url) {
|
|
136
|
+
throw new Error("Must provide either agentId (ERC-8004 global ID) or url");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let mcpUrl: string;
|
|
140
|
+
let verification: Awaited<ReturnType<typeof verifyAgent>> | undefined;
|
|
141
|
+
|
|
142
|
+
if (config.agentId) {
|
|
143
|
+
// Verify identity and resolve MCP endpoint
|
|
144
|
+
verification = await verifyAgent(config.agentId);
|
|
145
|
+
|
|
146
|
+
if (!verification.valid) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Cannot connect to agent "${config.agentId}": ${verification.error ?? "Identity verification failed"}`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const endpoint = await getMcpEndpoint(config.agentId);
|
|
153
|
+
if (!endpoint) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`Agent "${config.agentId}" does not expose an MCP endpoint. ` +
|
|
156
|
+
`Check their registration's services array for a "MCP" service entry.`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
mcpUrl = endpoint;
|
|
161
|
+
} else {
|
|
162
|
+
mcpUrl = config.url!;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Set up payment client if payer is configured
|
|
166
|
+
let payer: PaymentClient | undefined;
|
|
167
|
+
if (config.payer) {
|
|
168
|
+
payer = new PaymentClient({
|
|
169
|
+
account: config.payer.account,
|
|
170
|
+
maxAmountPerRequest: config.payer.maxAmount,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Create MCP client with optional payment-wrapped fetch
|
|
175
|
+
const client = new Client({
|
|
176
|
+
name: "a3stack-client",
|
|
177
|
+
version: "0.1.0",
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const transportFetch = payer ? payer.fetch : fetch;
|
|
181
|
+
const transport = new StreamableHTTPClientTransport(new URL(mcpUrl), {
|
|
182
|
+
// Use payment-aware fetch if payer configured
|
|
183
|
+
fetch: transportFetch as typeof fetch,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
await client.connect(transport);
|
|
187
|
+
|
|
188
|
+
return new AgentMcpClientInstance(client, transport, config, new URL(mcpUrl), payer, verification);
|
|
189
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @a3stack/data
|
|
3
|
+
* MCP server/client helpers with built-in identity verification and payment gating
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { AgentMcpServerInstance, createAgentMcpServer } from "./server.js";
|
|
7
|
+
export { AgentMcpClientInstance, createAgentMcpClient } from "./client.js";
|
|
8
|
+
export { probeAgent } from "./probe.js";
|
|
9
|
+
export type { AgentMcpServerConfig, AgentMcpClientConfig, AgentMcpServer } from "./types.js";
|
|
10
|
+
export type { AgentProbeResult } from "./probe.js";
|
package/src/probe.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* probeAgent — discover what an agent offers before connecting
|
|
3
|
+
*
|
|
4
|
+
* Resolves identity, checks payment requirements, lists available endpoints.
|
|
5
|
+
* All read-only, no wallet needed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { verifyAgent, getMcpEndpoint, getA2aEndpoint, parseAgentId } from "@a3stack/identity";
|
|
9
|
+
import type { AgentRegistrationFile, VerificationResult } from "@a3stack/identity";
|
|
10
|
+
|
|
11
|
+
export interface AgentProbeResult {
|
|
12
|
+
/** The global agent ID that was probed */
|
|
13
|
+
globalId: string;
|
|
14
|
+
|
|
15
|
+
/** Whether the agent's identity is verified on-chain */
|
|
16
|
+
verified: boolean;
|
|
17
|
+
|
|
18
|
+
/** The owner wallet address */
|
|
19
|
+
owner: string | null;
|
|
20
|
+
|
|
21
|
+
/** The payment wallet (or null if defaults to owner) */
|
|
22
|
+
paymentWallet: string | null;
|
|
23
|
+
|
|
24
|
+
/** The full registration file (if fetchable) */
|
|
25
|
+
registration: AgentRegistrationFile | null;
|
|
26
|
+
|
|
27
|
+
/** Resolved service endpoints */
|
|
28
|
+
endpoints: {
|
|
29
|
+
mcp: string | null;
|
|
30
|
+
a2a: string | null;
|
|
31
|
+
web: string | null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Whether the agent accepts x402 payments */
|
|
35
|
+
acceptsPayment: boolean;
|
|
36
|
+
|
|
37
|
+
/** Whether the agent is marked as active */
|
|
38
|
+
active: boolean;
|
|
39
|
+
|
|
40
|
+
/** All services exposed */
|
|
41
|
+
services: Array<{ name: string; endpoint: string; version?: string }>;
|
|
42
|
+
|
|
43
|
+
/** Cross-chain registrations */
|
|
44
|
+
registrations: Array<{ agentId: number; agentRegistry: string }>;
|
|
45
|
+
|
|
46
|
+
/** Payment details from probing the MCP endpoint (if reachable) */
|
|
47
|
+
paymentRequirements?: {
|
|
48
|
+
amount?: string;
|
|
49
|
+
network?: string;
|
|
50
|
+
payTo?: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Any error during probing */
|
|
54
|
+
error?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Probe an agent to discover its capabilities without connecting.
|
|
59
|
+
* Read-only operation — no wallet or payment needed.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* const info = await probeAgent("eip155:8453:0x8004...#2376");
|
|
64
|
+
* console.log(info.verified); // true
|
|
65
|
+
* console.log(info.endpoints.mcp); // "https://mcp.agent.eth/mcp"
|
|
66
|
+
* console.log(info.acceptsPayment); // true
|
|
67
|
+
* console.log(info.paymentRequirements); // { amount: "10000", network: "eip155:8453" }
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export async function probeAgent(globalId: string): Promise<AgentProbeResult> {
|
|
71
|
+
const ref = parseAgentId(globalId);
|
|
72
|
+
|
|
73
|
+
const result: AgentProbeResult = {
|
|
74
|
+
globalId,
|
|
75
|
+
verified: false,
|
|
76
|
+
owner: null,
|
|
77
|
+
paymentWallet: null,
|
|
78
|
+
registration: null,
|
|
79
|
+
endpoints: { mcp: null, a2a: null, web: null },
|
|
80
|
+
acceptsPayment: false,
|
|
81
|
+
active: false,
|
|
82
|
+
services: [],
|
|
83
|
+
registrations: [],
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Step 1: Verify on-chain identity
|
|
87
|
+
let verification: VerificationResult;
|
|
88
|
+
try {
|
|
89
|
+
verification = await verifyAgent(globalId);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
result.error = `Verification failed: ${(e as Error).message}`;
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
result.verified = verification.valid;
|
|
96
|
+
result.owner = verification.owner;
|
|
97
|
+
result.paymentWallet = verification.paymentWallet;
|
|
98
|
+
result.registration = verification.registration;
|
|
99
|
+
|
|
100
|
+
if (!verification.valid) {
|
|
101
|
+
result.error = verification.error ?? "Identity verification failed";
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Step 2: Extract registration data
|
|
106
|
+
if (verification.registration) {
|
|
107
|
+
const reg = verification.registration;
|
|
108
|
+
result.active = reg.active;
|
|
109
|
+
result.acceptsPayment = reg.x402Support;
|
|
110
|
+
result.services = reg.services ?? [];
|
|
111
|
+
result.registrations = reg.registrations ?? [];
|
|
112
|
+
|
|
113
|
+
// Resolve known endpoints
|
|
114
|
+
const mcpSvc = reg.services?.find(s => s.name.toUpperCase() === "MCP");
|
|
115
|
+
const a2aSvc = reg.services?.find(s => s.name.toUpperCase() === "A2A");
|
|
116
|
+
const webSvc = reg.services?.find(s => s.name.toLowerCase() === "web");
|
|
117
|
+
|
|
118
|
+
result.endpoints.mcp = mcpSvc?.endpoint ?? null;
|
|
119
|
+
result.endpoints.a2a = a2aSvc?.endpoint ?? null;
|
|
120
|
+
result.endpoints.web = webSvc?.endpoint ?? null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Step 3: If MCP endpoint exists and agent accepts payments, probe for requirements
|
|
124
|
+
if (result.endpoints.mcp && result.acceptsPayment) {
|
|
125
|
+
try {
|
|
126
|
+
const response = await fetch(result.endpoints.mcp, {
|
|
127
|
+
method: "POST",
|
|
128
|
+
headers: { "Content-Type": "application/json" },
|
|
129
|
+
body: JSON.stringify({ jsonrpc: "2.0", method: "ping", id: 1 }),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (response.status === 402) {
|
|
133
|
+
const reqHeader = response.headers.get("x-payment-required") ??
|
|
134
|
+
response.headers.get("X-PAYMENT-REQUIRED");
|
|
135
|
+
if (reqHeader) {
|
|
136
|
+
try {
|
|
137
|
+
const decoded = JSON.parse(Buffer.from(reqHeader, "base64").toString("utf8"));
|
|
138
|
+
const first = Array.isArray(decoded.accepts) ? decoded.accepts[0] : decoded;
|
|
139
|
+
result.paymentRequirements = {
|
|
140
|
+
amount: first.maxAmountRequired,
|
|
141
|
+
network: first.network,
|
|
142
|
+
payTo: first.payTo,
|
|
143
|
+
};
|
|
144
|
+
} catch { /* ignore parse errors */ }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
// MCP endpoint not reachable — that's fine, not an error
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return result;
|
|
153
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createAgentMcpServer — MCP server with built-in identity + payment support
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { McpServer, type RegisteredTool as McpRegisteredTool } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
9
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
+
import { createPublicClient, http } from "viem";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import {
|
|
13
|
+
fetchRegistrationFile,
|
|
14
|
+
IDENTITY_REGISTRY_ADDRESS,
|
|
15
|
+
IDENTITY_REGISTRY_ABI,
|
|
16
|
+
SUPPORTED_CHAINS,
|
|
17
|
+
} from "@a3stack/identity";
|
|
18
|
+
import { PaymentServer, USDC_BASE, DEFAULT_NETWORK } from "@a3stack/payments";
|
|
19
|
+
import type { AgentMcpServerConfig } from "./types.js";
|
|
20
|
+
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
|
+
type ToolHandler<T = any> = (args: T) => Promise<CallToolResult>;
|
|
23
|
+
|
|
24
|
+
interface InternalRegisteredTool {
|
|
25
|
+
requiresPayment: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Agent MCP Server wrapping @modelcontextprotocol/sdk with payment + identity support
|
|
30
|
+
*/
|
|
31
|
+
export class AgentMcpServerInstance {
|
|
32
|
+
private config: Required<Omit<AgentMcpServerConfig, "payment" | "identity">> & {
|
|
33
|
+
payment?: AgentMcpServerConfig["payment"];
|
|
34
|
+
identity?: AgentMcpServerConfig["identity"];
|
|
35
|
+
};
|
|
36
|
+
private mcp: McpServer;
|
|
37
|
+
private paymentServer?: PaymentServer;
|
|
38
|
+
private tools = new Map<string, InternalRegisteredTool>();
|
|
39
|
+
private httpServer?: ReturnType<typeof createServer>;
|
|
40
|
+
|
|
41
|
+
constructor(config: AgentMcpServerConfig) {
|
|
42
|
+
this.config = {
|
|
43
|
+
name: config.name,
|
|
44
|
+
version: config.version,
|
|
45
|
+
port: config.port ?? 3000,
|
|
46
|
+
cors: config.cors ?? true,
|
|
47
|
+
identity: config.identity,
|
|
48
|
+
payment: config.payment,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
this.mcp = new McpServer({
|
|
52
|
+
name: config.name,
|
|
53
|
+
version: config.version,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (config.payment) {
|
|
57
|
+
this.paymentServer = new PaymentServer({
|
|
58
|
+
payTo: config.payment.payTo,
|
|
59
|
+
amount: config.payment.amount,
|
|
60
|
+
asset: config.payment.asset ?? USDC_BASE,
|
|
61
|
+
network: config.payment.network ?? DEFAULT_NETWORK,
|
|
62
|
+
description: config.payment.description ?? `${config.name} MCP service`,
|
|
63
|
+
maxTimeoutSeconds: config.payment.maxTimeoutSeconds,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Register built-in tools
|
|
68
|
+
this._registerBuiltinTools();
|
|
69
|
+
|
|
70
|
+
// Register identity resource if configured
|
|
71
|
+
if (config.identity) {
|
|
72
|
+
this._registerIdentityResource();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private _registerBuiltinTools() {
|
|
77
|
+
// Ping — always free
|
|
78
|
+
this.mcp.tool(
|
|
79
|
+
"ping",
|
|
80
|
+
"Check if the server is alive",
|
|
81
|
+
{},
|
|
82
|
+
async () => ({
|
|
83
|
+
content: [
|
|
84
|
+
{
|
|
85
|
+
type: "text" as const,
|
|
86
|
+
text: JSON.stringify({
|
|
87
|
+
status: "ok",
|
|
88
|
+
server: this.config.name,
|
|
89
|
+
version: this.config.version,
|
|
90
|
+
timestamp: new Date().toISOString(),
|
|
91
|
+
requiresPayment: !!this.config.payment,
|
|
92
|
+
...(this.config.payment
|
|
93
|
+
? {
|
|
94
|
+
paymentInfo: {
|
|
95
|
+
amount: this.config.payment.amount,
|
|
96
|
+
network: this.config.payment.network ?? DEFAULT_NETWORK,
|
|
97
|
+
payTo: this.config.payment.payTo,
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
: {}),
|
|
101
|
+
}),
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
})
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private _registerIdentityResource() {
|
|
109
|
+
const identityConfig = this.config.identity!;
|
|
110
|
+
|
|
111
|
+
// Register as MCP resource at agent://identity
|
|
112
|
+
this.mcp.resource(
|
|
113
|
+
"agent-identity",
|
|
114
|
+
"agent://identity",
|
|
115
|
+
{ description: "The agent's ERC-8004 on-chain identity registration", mimeType: "application/json" },
|
|
116
|
+
async (_uri: URL) => {
|
|
117
|
+
try {
|
|
118
|
+
const chainDefaults = SUPPORTED_CHAINS[identityConfig.chainId];
|
|
119
|
+
const rpcUrl = identityConfig.rpc ?? chainDefaults?.rpc;
|
|
120
|
+
|
|
121
|
+
if (!rpcUrl) {
|
|
122
|
+
return {
|
|
123
|
+
contents: [
|
|
124
|
+
{
|
|
125
|
+
uri: "agent://identity",
|
|
126
|
+
text: JSON.stringify({
|
|
127
|
+
error: `No RPC available for chain ${identityConfig.chainId}`,
|
|
128
|
+
}),
|
|
129
|
+
mimeType: "application/json",
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const client = createPublicClient({ transport: http(rpcUrl) });
|
|
136
|
+
const registry = identityConfig.registry ?? IDENTITY_REGISTRY_ADDRESS;
|
|
137
|
+
|
|
138
|
+
const agentUri = (await client.readContract({
|
|
139
|
+
address: registry,
|
|
140
|
+
abi: IDENTITY_REGISTRY_ABI,
|
|
141
|
+
functionName: "tokenURI",
|
|
142
|
+
args: [BigInt(identityConfig.agentId)],
|
|
143
|
+
})) as string;
|
|
144
|
+
|
|
145
|
+
const registration = await fetchRegistrationFile(agentUri);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
contents: [
|
|
149
|
+
{
|
|
150
|
+
uri: "agent://identity",
|
|
151
|
+
text: JSON.stringify(
|
|
152
|
+
{
|
|
153
|
+
globalId: `eip155:${identityConfig.chainId}:${registry}#${identityConfig.agentId}`,
|
|
154
|
+
...registration,
|
|
155
|
+
},
|
|
156
|
+
null,
|
|
157
|
+
2
|
|
158
|
+
),
|
|
159
|
+
mimeType: "application/json",
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
};
|
|
163
|
+
} catch (e) {
|
|
164
|
+
return {
|
|
165
|
+
contents: [
|
|
166
|
+
{
|
|
167
|
+
uri: "agent://identity",
|
|
168
|
+
text: JSON.stringify({ error: (e as Error).message }),
|
|
169
|
+
mimeType: "application/json",
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Register a tool (wraps McpServer.tool())
|
|
180
|
+
* The handler must return a CallToolResult-compatible object.
|
|
181
|
+
*/
|
|
182
|
+
tool(
|
|
183
|
+
name: string,
|
|
184
|
+
description: string,
|
|
185
|
+
paramsSchema: Record<string, z.ZodTypeAny>,
|
|
186
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
187
|
+
handler: ToolHandler<any>
|
|
188
|
+
): McpRegisteredTool {
|
|
189
|
+
const freeTools = this.config.payment?.freeTools ?? ["ping"];
|
|
190
|
+
this.tools.set(name, {
|
|
191
|
+
requiresPayment: !!this.config.payment && !freeTools.includes(name),
|
|
192
|
+
});
|
|
193
|
+
return this.mcp.tool(name, description, paramsSchema, handler);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Register a resource (wraps McpServer.resource())
|
|
198
|
+
*/
|
|
199
|
+
resource(
|
|
200
|
+
name: string,
|
|
201
|
+
uri: string,
|
|
202
|
+
description: string,
|
|
203
|
+
handler: (uri: URL) => Promise<{ contents: Array<{ uri: string; text: string; mimeType?: string }> }>
|
|
204
|
+
): void {
|
|
205
|
+
this.mcp.resource(name, uri, { description }, handler);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Start the HTTP server
|
|
210
|
+
*/
|
|
211
|
+
async listen(port?: number): Promise<{ url: string }> {
|
|
212
|
+
const listenPort = port ?? this.config.port;
|
|
213
|
+
const transports = new Map<string, StreamableHTTPServerTransport>();
|
|
214
|
+
|
|
215
|
+
this.httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
216
|
+
// CORS headers
|
|
217
|
+
if (this.config.cors) {
|
|
218
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
219
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
220
|
+
res.setHeader(
|
|
221
|
+
"Access-Control-Allow-Headers",
|
|
222
|
+
"Content-Type, MCP-Session-Id, X-PAYMENT, X-PAYMENT-REQUIRED"
|
|
223
|
+
);
|
|
224
|
+
if (req.method === "OPTIONS") {
|
|
225
|
+
res.writeHead(204);
|
|
226
|
+
res.end();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Only handle /mcp path
|
|
232
|
+
if (req.url !== "/mcp" && req.url !== "/") {
|
|
233
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
234
|
+
res.end(JSON.stringify({ error: "Not found", hint: "MCP endpoint is at /mcp" }));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Check payment for POST requests (tool calls)
|
|
239
|
+
if (req.method === "POST" && this.paymentServer) {
|
|
240
|
+
const paymentHeader = req.headers["x-payment"] as string | undefined;
|
|
241
|
+
|
|
242
|
+
if (!paymentHeader) {
|
|
243
|
+
const requirementsHeader = this.paymentServer.buildRequirementsHeader(
|
|
244
|
+
`https://${req.headers.host ?? "localhost"}${req.url ?? "/mcp"}`
|
|
245
|
+
);
|
|
246
|
+
res.writeHead(402, {
|
|
247
|
+
"Content-Type": "application/json",
|
|
248
|
+
"X-PAYMENT-REQUIRED": requirementsHeader,
|
|
249
|
+
});
|
|
250
|
+
res.end(
|
|
251
|
+
JSON.stringify({
|
|
252
|
+
error: "Payment Required",
|
|
253
|
+
message:
|
|
254
|
+
`This MCP server requires x402 payment. ` +
|
|
255
|
+
`Use a payment-capable client or pay ${this.config.payment!.amount} USDC on ${this.config.payment!.network ?? DEFAULT_NETWORK}.`,
|
|
256
|
+
})
|
|
257
|
+
);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const payResult = await this.paymentServer.verify(req);
|
|
262
|
+
if (!payResult.valid) {
|
|
263
|
+
res.writeHead(402, { "Content-Type": "application/json" });
|
|
264
|
+
res.end(
|
|
265
|
+
JSON.stringify({ error: "Invalid Payment", message: payResult.error })
|
|
266
|
+
);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Handle MCP protocol
|
|
272
|
+
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
|
273
|
+
let transport: StreamableHTTPServerTransport;
|
|
274
|
+
|
|
275
|
+
if (sessionId && transports.has(sessionId)) {
|
|
276
|
+
transport = transports.get(sessionId)!;
|
|
277
|
+
} else if (!sessionId && req.method === "POST") {
|
|
278
|
+
// New session
|
|
279
|
+
const newSessionId = randomUUID();
|
|
280
|
+
transport = new StreamableHTTPServerTransport({
|
|
281
|
+
sessionIdGenerator: () => newSessionId,
|
|
282
|
+
});
|
|
283
|
+
transports.set(newSessionId, transport);
|
|
284
|
+
transport.onclose = () => {
|
|
285
|
+
transports.delete(newSessionId);
|
|
286
|
+
};
|
|
287
|
+
await this.mcp.connect(transport);
|
|
288
|
+
} else if (req.method === "GET") {
|
|
289
|
+
// SSE connection for existing session — create standalone transport for streaming
|
|
290
|
+
transport = new StreamableHTTPServerTransport({
|
|
291
|
+
sessionIdGenerator: undefined, // stateless for GET
|
|
292
|
+
});
|
|
293
|
+
await this.mcp.connect(transport);
|
|
294
|
+
} else {
|
|
295
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
296
|
+
res.end(JSON.stringify({ error: "Bad Request", message: "Missing MCP-Session-Id" }));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
await transport.handleRequest(req, res);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
await new Promise<void>((resolve) => {
|
|
304
|
+
this.httpServer!.listen(listenPort, resolve);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const url = `http://localhost:${listenPort}/mcp`;
|
|
308
|
+
return { url };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Stop the server
|
|
313
|
+
*/
|
|
314
|
+
async close(): Promise<void> {
|
|
315
|
+
await this.mcp.close();
|
|
316
|
+
if (this.httpServer) {
|
|
317
|
+
await new Promise<void>((resolve, reject) => {
|
|
318
|
+
this.httpServer!.close((err) => (err ? reject(err) : resolve()));
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Access the raw McpServer for advanced usage */
|
|
324
|
+
get server(): McpServer {
|
|
325
|
+
return this.mcp;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Create an MCP server with built-in identity + payment support
|
|
331
|
+
*/
|
|
332
|
+
export function createAgentMcpServer(
|
|
333
|
+
config: AgentMcpServerConfig
|
|
334
|
+
): AgentMcpServerInstance {
|
|
335
|
+
return new AgentMcpServerInstance(config);
|
|
336
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @a3stack/data — Type Definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { PaymentServerConfig } from "@a3stack/payments";
|
|
6
|
+
|
|
7
|
+
export interface AgentMcpServerConfig {
|
|
8
|
+
/** MCP server name (shown to clients) */
|
|
9
|
+
name: string;
|
|
10
|
+
/** MCP server version */
|
|
11
|
+
version: string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Auto-expose the agent's ERC-8004 identity as an MCP resource.
|
|
15
|
+
* If provided, adds an "agent://identity" resource.
|
|
16
|
+
*/
|
|
17
|
+
identity?: {
|
|
18
|
+
chainId: number;
|
|
19
|
+
agentId: number;
|
|
20
|
+
registry?: `0x${string}`;
|
|
21
|
+
rpc?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Require x402 payment before serving paid tools.
|
|
26
|
+
* Free tools (like ping, identity) are always accessible.
|
|
27
|
+
*/
|
|
28
|
+
payment?: PaymentServerConfig & {
|
|
29
|
+
/** Tool names that don't require payment. Defaults to ["ping"] */
|
|
30
|
+
freeTools?: string[];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Port to listen on. Defaults to 3000. */
|
|
34
|
+
port?: number;
|
|
35
|
+
|
|
36
|
+
/** Enable CORS. Defaults to true. */
|
|
37
|
+
cors?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AgentMcpClientConfig {
|
|
41
|
+
/**
|
|
42
|
+
* Connect by ERC-8004 global ID — auto-resolves MCP endpoint.
|
|
43
|
+
* Format: "eip155:{chainId}:{registry}#{agentId}"
|
|
44
|
+
*/
|
|
45
|
+
agentId?: string;
|
|
46
|
+
|
|
47
|
+
/** Connect by direct MCP URL */
|
|
48
|
+
url?: string;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* If the server requires x402 payment, configure a payer.
|
|
52
|
+
* If not provided, the client will fail on 402 responses.
|
|
53
|
+
*/
|
|
54
|
+
payer?: {
|
|
55
|
+
/** viem Account (e.g. from privateKeyToAccount()) */
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
57
|
+
account: any;
|
|
58
|
+
/** Max USDC to auto-pay per session. Default: 10 USDC */
|
|
59
|
+
maxAmount?: string;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface AgentMcpServer {
|
|
64
|
+
/** The underlying McpServer instance */
|
|
65
|
+
mcp: unknown;
|
|
66
|
+
/**
|
|
67
|
+
* Register a tool (passthrough to McpServer.tool())
|
|
68
|
+
*/
|
|
69
|
+
tool(name: string, paramsSchema: unknown, handler: unknown): void;
|
|
70
|
+
/**
|
|
71
|
+
* Register a resource (passthrough to McpServer.resource())
|
|
72
|
+
*/
|
|
73
|
+
resource(name: string, uri: string, handler: unknown): void;
|
|
74
|
+
/**
|
|
75
|
+
* Start listening
|
|
76
|
+
*/
|
|
77
|
+
listen(port?: number): Promise<{ url: string }>;
|
|
78
|
+
/**
|
|
79
|
+
* Stop the server
|
|
80
|
+
*/
|
|
81
|
+
close(): Promise<void>;
|
|
82
|
+
}
|