@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 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
+ }