nullpath-mcp 1.1.0 → 1.2.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/src/index.ts CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  /**
4
4
  * nullpath MCP Client
5
- *
6
- * Connects to nullpath.com/mcp - AI agent marketplace with x402 micropayments.
7
- *
5
+ *
6
+ * Connects to nullpath.com API - AI agent marketplace with x402 micropayments.
7
+ *
8
8
  * Available tools:
9
9
  * - discover_agents: Search agents by capability
10
10
  * - lookup_agent: Get agent details by ID
@@ -14,249 +14,223 @@
14
14
  * - check_reputation: Get agent trust score
15
15
  */
16
16
 
17
- import { Client } from '@modelcontextprotocol/sdk/client/index.js';
18
- import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
19
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
20
17
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
18
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
21
19
  import {
22
- ListToolsRequestSchema,
23
20
  CallToolRequestSchema,
21
+ ListToolsRequestSchema,
24
22
  } from '@modelcontextprotocol/sdk/types.js';
25
- import { privateKeyToAccount } from 'viem/accounts';
26
- import type { Address, Hex } from 'viem';
27
-
28
- const NULLPATH_MCP_URL = process.env.NULLPATH_MCP_URL || 'https://nullpath.com/mcp';
29
-
30
- /**
31
- * Payment requirements from x402 402 response
32
- */
33
- export interface PaymentRequirements {
34
- scheme: 'exact';
35
- network: 'base' | 'base-sepolia';
36
- maxAmountRequired: string;
37
- resource: string;
38
- description: string;
39
- mimeType: string;
40
- payTo: Address;
41
- maxTimeoutSeconds: number;
42
- asset: Address;
43
- extra: Record<string, unknown>;
44
- }
45
23
 
46
- /**
47
- * x402 error response structure from MCP server
48
- */
49
- export interface X402ErrorData {
50
- x402Version: number;
51
- error: string;
52
- accepts: PaymentRequirements[];
53
- priceBreakdown?: {
54
- platformFee: number;
55
- agentFee: number;
56
- platformCut: number;
57
- agentEarnings: number;
58
- total: number;
59
- currency: string;
60
- };
61
- }
62
-
63
- /**
64
- * EIP-3009 TransferWithAuthorization typed data
65
- */
66
- const TRANSFER_WITH_AUTHORIZATION_TYPES = {
67
- TransferWithAuthorization: [
68
- { name: 'from', type: 'address' },
69
- { name: 'to', type: 'address' },
70
- { name: 'value', type: 'uint256' },
71
- { name: 'validAfter', type: 'uint256' },
72
- { name: 'validBefore', type: 'uint256' },
73
- { name: 'nonce', type: 'bytes32' },
74
- ],
75
- } as const;
76
-
77
- /**
78
- * USDC contract metadata by network
79
- */
80
- const USDC_CONTRACTS: Record<string, { name: string; version: string; chainId: number }> = {
81
- 'base': { name: 'USD Coin', version: '2', chainId: 8453 },
82
- 'base-sepolia': { name: 'USD Coin', version: '2', chainId: 84532 },
83
- };
84
-
85
- /**
86
- * Payment payload structure
87
- */
88
- export interface PaymentPayload {
89
- x402Version: number;
90
- scheme: 'exact';
91
- network: string;
92
- payload: {
93
- signature: `0x${string}`;
94
- authorization: {
95
- from: Address;
96
- to: Address;
97
- value: string;
98
- validAfter: string;
99
- validBefore: string;
100
- nonce: `0x${string}`;
101
- };
102
- };
24
+ const NULLPATH_API_URL = process.env.NULLPATH_API_URL || 'https://nullpath.com/api/v1';
25
+
26
+ // Tool definitions
27
+ const TOOLS = [
28
+ {
29
+ name: 'discover_agents',
30
+ description: 'Search for agents by capability, category, or query. Returns a list of matching agents with their pricing and reputation.',
31
+ inputSchema: {
32
+ type: 'object' as const,
33
+ properties: {
34
+ query: { type: 'string', description: 'Search query (e.g., "summarize", "translate", "code review")' },
35
+ category: { type: 'string', description: 'Filter by category (e.g., "text", "code", "data")' },
36
+ limit: { type: 'number', description: 'Maximum results to return (default: 10)' },
37
+ },
38
+ },
39
+ },
40
+ {
41
+ name: 'lookup_agent',
42
+ description: 'Get detailed information about a specific agent by ID, including capabilities, pricing, and reputation.',
43
+ inputSchema: {
44
+ type: 'object' as const,
45
+ properties: {
46
+ agentId: { type: 'string', description: 'The agent UUID' },
47
+ },
48
+ required: ['agentId'],
49
+ },
50
+ },
51
+ {
52
+ name: 'get_capabilities',
53
+ description: 'List all capability categories available in the marketplace.',
54
+ inputSchema: {
55
+ type: 'object' as const,
56
+ properties: {},
57
+ },
58
+ },
59
+ {
60
+ name: 'check_reputation',
61
+ description: 'Get the reputation score and trust tier for an agent.',
62
+ inputSchema: {
63
+ type: 'object' as const,
64
+ properties: {
65
+ agentId: { type: 'string', description: 'The agent UUID' },
66
+ },
67
+ required: ['agentId'],
68
+ },
69
+ },
70
+ {
71
+ name: 'execute_agent',
72
+ description: 'Execute an agent capability. Requires payment via x402 (USDC on Base). Set NULLPATH_WALLET_KEY env var for payments.',
73
+ inputSchema: {
74
+ type: 'object' as const,
75
+ properties: {
76
+ agentId: { type: 'string', description: 'The agent UUID' },
77
+ capabilityId: { type: 'string', description: 'The capability to execute' },
78
+ input: { type: 'object', description: 'Input parameters for the capability' },
79
+ },
80
+ required: ['agentId', 'capabilityId', 'input'],
81
+ },
82
+ },
83
+ {
84
+ name: 'register_agent',
85
+ description: 'Register a new agent on the marketplace. Requires $0.10 USDC payment. Set NULLPATH_WALLET_KEY env var.',
86
+ inputSchema: {
87
+ type: 'object' as const,
88
+ properties: {
89
+ name: { type: 'string', description: 'Agent name' },
90
+ description: { type: 'string', description: 'Agent description' },
91
+ wallet: { type: 'string', description: 'Wallet address for receiving payments' },
92
+ capabilities: {
93
+ type: 'array',
94
+ description: 'List of capabilities with pricing',
95
+ items: {
96
+ type: 'object',
97
+ properties: {
98
+ id: { type: 'string' },
99
+ name: { type: 'string' },
100
+ description: { type: 'string' },
101
+ price: { type: 'string', description: 'Price in USDC (e.g., "0.01")' },
102
+ },
103
+ },
104
+ },
105
+ endpoint: { type: 'string', description: 'Execution endpoint URL' },
106
+ },
107
+ required: ['name', 'description', 'wallet', 'capabilities', 'endpoint'],
108
+ },
109
+ },
110
+ ];
111
+
112
+ // API helper
113
+ async function apiCall(endpoint: string, options: RequestInit = {}): Promise<unknown> {
114
+ const url = `${NULLPATH_API_URL}${endpoint}`;
115
+ const response = await fetch(url, {
116
+ ...options,
117
+ headers: {
118
+ 'Content-Type': 'application/json',
119
+ ...options.headers,
120
+ },
121
+ });
122
+
123
+ if (!response.ok) {
124
+ const error = await response.text();
125
+ throw new Error(`API error (${response.status}): ${error}`);
126
+ }
127
+
128
+ return response.json();
103
129
  }
104
130
 
105
- /**
106
- * Generate a random 32-byte nonce for EIP-3009
107
- */
108
- function generateNonce(): `0x${string}` {
109
- const bytes = new Uint8Array(32);
110
- crypto.getRandomValues(bytes);
111
- return `0x${Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')}` as `0x${string}`;
131
+ // Tool handlers
132
+ async function handleDiscoverAgents(args: { query?: string; category?: string; limit?: number }) {
133
+ const params = new URLSearchParams();
134
+ if (args.query) params.set('q', args.query);
135
+ if (args.category) params.set('category', args.category);
136
+ if (args.limit) params.set('limit', args.limit.toString());
137
+
138
+ const queryString = params.toString();
139
+ const endpoint = `/discover${queryString ? `?${queryString}` : ''}`;
140
+
141
+ return apiCall(endpoint);
112
142
  }
113
143
 
114
- /**
115
- * Wallet with address and signing capability
116
- */
117
- export interface Wallet {
118
- address: Address;
119
- privateKey: Hex;
144
+ async function handleLookupAgent(args: { agentId: string }) {
145
+ return apiCall(`/agents/${args.agentId}`);
120
146
  }
121
147
 
122
- /**
123
- * Sign an EIP-3009 TransferWithAuthorization
124
- */
125
- export async function signTransferAuthorization(
126
- wallet: Wallet,
127
- requirements: PaymentRequirements
128
- ): Promise<PaymentPayload> {
129
- const network = requirements.network;
130
- const contractMeta = USDC_CONTRACTS[network];
148
+ async function handleGetCapabilities() {
149
+ // Return the capability categories from discover endpoint
150
+ const result = await apiCall('/discover') as { data?: { agents?: Array<{ capabilities?: unknown[] }> } };
151
+ const agents = result?.data?.agents || [];
152
+ const categories = new Set<string>();
131
153
 
132
- if (!contractMeta) {
133
- throw new Error(`Unsupported network: ${network}`);
154
+ for (const agent of agents) {
155
+ if (agent.capabilities) {
156
+ for (const cap of agent.capabilities as Array<{ id?: string }>) {
157
+ if (cap.id) categories.add(cap.id.split('-')[0]);
158
+ }
159
+ }
134
160
  }
161
+
162
+ return { categories: Array.from(categories) };
163
+ }
135
164
 
136
- const now = Math.floor(Date.now() / 1000);
137
- const validAfter = now - 60; // Valid from 1 minute ago
138
- const validBefore = now + requirements.maxTimeoutSeconds;
139
- const nonce = generateNonce();
140
-
141
- const authorization = {
142
- from: wallet.address,
143
- to: requirements.payTo,
144
- value: BigInt(requirements.maxAmountRequired),
145
- validAfter: BigInt(validAfter),
146
- validBefore: BigInt(validBefore),
147
- nonce,
165
+ async function handleCheckReputation(args: { agentId: string }) {
166
+ const result = await apiCall(`/agents/${args.agentId}`) as {
167
+ data?: {
168
+ reputation_score?: number;
169
+ trustTier?: string;
170
+ avgLatencyMs?: number;
171
+ }
148
172
  };
149
-
150
- // Create account for signing
151
- const account = privateKeyToAccount(wallet.privateKey);
152
173
 
153
- const signature = await account.signTypedData({
154
- domain: {
155
- name: contractMeta.name,
156
- version: contractMeta.version,
157
- chainId: contractMeta.chainId,
158
- verifyingContract: requirements.asset,
159
- },
160
- types: TRANSFER_WITH_AUTHORIZATION_TYPES,
161
- primaryType: 'TransferWithAuthorization',
162
- message: authorization,
163
- });
164
-
165
174
  return {
166
- x402Version: 1,
167
- scheme: 'exact',
168
- network,
169
- payload: {
170
- signature,
171
- authorization: {
172
- from: wallet.address,
173
- to: requirements.payTo,
174
- value: authorization.value.toString(),
175
- validAfter: authorization.validAfter.toString(),
176
- validBefore: authorization.validBefore.toString(),
177
- nonce,
178
- },
179
- },
175
+ agentId: args.agentId,
176
+ reputationScore: result?.data?.reputation_score,
177
+ trustTier: result?.data?.trustTier,
178
+ avgLatencyMs: result?.data?.avgLatencyMs,
180
179
  };
181
180
  }
182
181
 
183
- /**
184
- * Encode payment payload to base64 for X-PAYMENT header
185
- */
186
- export function encodePaymentHeader(payment: PaymentPayload): string {
187
- return Buffer.from(JSON.stringify(payment)).toString('base64');
188
- }
189
-
190
- /**
191
- * Check if an error response is an x402 payment required error
192
- */
193
- export function isX402Error(error: unknown): error is { code: number; message: string; data: X402ErrorData } {
194
- if (typeof error !== 'object' || error === null) return false;
195
- const err = error as Record<string, unknown>;
196
- if (err.code !== -32000) return false;
197
- if (typeof err.data !== 'object' || err.data === null) return false;
198
- const data = err.data as Record<string, unknown>;
199
- return typeof data.x402Version === 'number' && Array.isArray(data.accepts);
200
- }
201
-
202
- /**
203
- * Get wallet from environment variable
204
- */
205
- export function getWallet(): Wallet {
206
- const privateKey = process.env.NULLPATH_WALLET_KEY;
182
+ async function handleExecuteAgent(args: { agentId: string; capabilityId: string; input: unknown }) {
183
+ // Note: This would need x402 payment handling for production
184
+ // For now, return info about what would happen
185
+ const walletKey = process.env.NULLPATH_WALLET_KEY;
207
186
 
208
- if (!privateKey) {
209
- throw new Error(
210
- 'NULLPATH_WALLET_KEY environment variable is required for paid tool calls. ' +
211
- 'Set it to your wallet private key (0x-prefixed hex string).'
212
- );
187
+ if (!walletKey) {
188
+ return {
189
+ error: 'NULLPATH_WALLET_KEY environment variable not set. Payment required for execution.',
190
+ info: 'Set your wallet private key to enable paid agent execution.',
191
+ };
213
192
  }
214
-
215
- // Ensure the key is properly formatted
216
- const formattedKey = privateKey.startsWith('0x')
217
- ? privateKey as Hex
218
- : `0x${privateKey}` as Hex;
219
-
220
- const account = privateKeyToAccount(formattedKey);
221
193
 
222
- return {
223
- address: account.address,
224
- privateKey: formattedKey,
225
- };
194
+ // TODO: Implement full x402 payment flow
195
+ return apiCall('/execute', {
196
+ method: 'POST',
197
+ body: JSON.stringify({
198
+ targetAgentId: args.agentId,
199
+ capabilityId: args.capabilityId,
200
+ input: args.input,
201
+ }),
202
+ });
226
203
  }
227
204
 
228
- /**
229
- * Handle x402 payment flow
230
- *
231
- * 1. Parse payment requirements from 402 error
232
- * 2. Sign EIP-3009 authorization
233
- * 3. Return payment header for retry
234
- */
235
- export async function handleX402Payment(
236
- errorData: X402ErrorData
237
- ): Promise<string> {
238
- const wallet = getWallet();
205
+ async function handleRegisterAgent(args: {
206
+ name: string;
207
+ description: string;
208
+ wallet: string;
209
+ capabilities: unknown[];
210
+ endpoint: string;
211
+ }) {
212
+ const walletKey = process.env.NULLPATH_WALLET_KEY;
239
213
 
240
- if (errorData.accepts.length === 0) {
241
- throw new Error('No payment options available in 402 response');
214
+ if (!walletKey) {
215
+ return {
216
+ error: 'NULLPATH_WALLET_KEY environment variable not set. Payment required for registration.',
217
+ info: 'Registration costs $0.10 USDC. Set your wallet private key to proceed.',
218
+ };
242
219
  }
243
-
244
- // Use the first payment option
245
- const requirements = errorData.accepts[0];
246
220
 
247
- // Sign the payment authorization
248
- const payment = await signTransferAuthorization(wallet, requirements);
249
-
250
- // Encode for header
251
- return encodePaymentHeader(payment);
221
+ // TODO: Implement full x402 payment flow
222
+ return apiCall('/agents', {
223
+ method: 'POST',
224
+ body: JSON.stringify(args),
225
+ });
252
226
  }
253
227
 
228
+ // Main server
254
229
  async function main() {
255
- // Create a local stdio server that proxies to nullpath's remote MCP
256
230
  const server = new Server(
257
231
  {
258
232
  name: 'nullpath-mcp',
259
- version: '1.0.0',
233
+ version: '1.2.0',
260
234
  },
261
235
  {
262
236
  capabilities: {
@@ -265,126 +239,77 @@ async function main() {
265
239
  }
266
240
  );
267
241
 
268
- // Connect to remote nullpath MCP server
269
- const transport = new SSEClientTransport(new URL(NULLPATH_MCP_URL));
270
- const client = new Client(
271
- {
272
- name: 'nullpath-mcp-proxy',
273
- version: '1.0.0',
274
- },
275
- {
276
- capabilities: {},
277
- }
278
- );
279
-
280
- await client.connect(transport);
281
-
282
- // List available tools from remote server
283
- const tools = await client.listTools();
284
-
285
- // Register tool handlers that proxy to remote
242
+ // List tools handler
286
243
  server.setRequestHandler(ListToolsRequestSchema, async () => {
287
- return tools;
244
+ return { tools: TOOLS };
288
245
  });
289
246
 
247
+ // Call tool handler
290
248
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
249
+ const { name, arguments: args } = request.params;
250
+
291
251
  try {
292
- // First attempt without payment
293
- const result = await client.callTool({
294
- name: request.params.name,
295
- arguments: request.params.arguments,
296
- });
297
- return result;
298
- } catch (error: unknown) {
299
- // Check if this is a 402 Payment Required error
300
- if (isX402Error(error)) {
301
- console.error(`Payment required for tool: ${request.params.name}`);
302
-
303
- try {
304
- // Handle x402 payment
305
- const paymentHeader = await handleX402Payment(error.data);
306
-
307
- // Retry with payment header
308
- // Note: The MCP SDK doesn't directly support custom headers on tool calls,
309
- // so we need to make the request ourselves with the payment header
310
- const retryResult = await retryWithPayment(
311
- request.params.name,
312
- request.params.arguments,
313
- paymentHeader
314
- );
315
-
316
- return retryResult;
317
- } catch (paymentError) {
318
- // Re-throw with more context
319
- throw new Error(
320
- `Payment failed for tool ${request.params.name}: ${
321
- paymentError instanceof Error ? paymentError.message : String(paymentError)
322
- }`
323
- );
324
- }
252
+ let result: unknown;
253
+
254
+ switch (name) {
255
+ case 'discover_agents':
256
+ result = await handleDiscoverAgents(args as { query?: string; category?: string; limit?: number });
257
+ break;
258
+ case 'lookup_agent':
259
+ result = await handleLookupAgent(args as { agentId: string });
260
+ break;
261
+ case 'get_capabilities':
262
+ result = await handleGetCapabilities();
263
+ break;
264
+ case 'check_reputation':
265
+ result = await handleCheckReputation(args as { agentId: string });
266
+ break;
267
+ case 'execute_agent':
268
+ result = await handleExecuteAgent(args as { agentId: string; capabilityId: string; input: unknown });
269
+ break;
270
+ case 'register_agent':
271
+ result = await handleRegisterAgent(args as {
272
+ name: string;
273
+ description: string;
274
+ wallet: string;
275
+ capabilities: unknown[];
276
+ endpoint: string;
277
+ });
278
+ break;
279
+ default:
280
+ throw new Error(`Unknown tool: ${name}`);
325
281
  }
326
-
327
- // Re-throw non-payment errors
328
- throw error;
329
- }
330
- });
331
-
332
- // Start local stdio transport
333
- const stdioTransport = new StdioServerTransport();
334
- await server.connect(stdioTransport);
335
-
336
- console.error('nullpath MCP client connected to', NULLPATH_MCP_URL);
337
- }
338
282
 
339
- /**
340
- * Retry a tool call with x402 payment header
341
- *
342
- * Makes a direct HTTP request to the MCP server with the X-PAYMENT header
343
- */
344
- async function retryWithPayment(
345
- toolName: string,
346
- args: Record<string, unknown> | undefined,
347
- paymentHeader: string
348
- ): Promise<{ content: Array<{ type: string; text: string }> }> {
349
- const requestBody = {
350
- jsonrpc: '2.0',
351
- method: 'tools/call',
352
- params: {
353
- name: toolName,
354
- arguments: args || {},
355
- },
356
- id: Date.now(),
357
- };
358
-
359
- const response = await fetch(NULLPATH_MCP_URL, {
360
- method: 'POST',
361
- headers: {
362
- 'Content-Type': 'application/json',
363
- 'X-PAYMENT': paymentHeader,
364
- },
365
- body: JSON.stringify(requestBody),
283
+ return {
284
+ content: [
285
+ {
286
+ type: 'text',
287
+ text: JSON.stringify(result, null, 2),
288
+ },
289
+ ],
290
+ };
291
+ } catch (error) {
292
+ const message = error instanceof Error ? error.message : String(error);
293
+ return {
294
+ content: [
295
+ {
296
+ type: 'text',
297
+ text: `Error: ${message}`,
298
+ },
299
+ ],
300
+ isError: true,
301
+ };
302
+ }
366
303
  });
367
304
 
368
- if (!response.ok) {
369
- const errorText = await response.text();
370
- throw new Error(`HTTP ${response.status}: ${errorText}`);
371
- }
372
-
373
- const result = await response.json() as {
374
- jsonrpc: string;
375
- result?: { content: Array<{ type: string; text: string }> };
376
- error?: { code: number; message: string; data?: unknown };
377
- id: number;
378
- };
379
-
380
- if (result.error) {
381
- throw new Error(`RPC Error: ${result.error.message}`);
382
- }
383
-
384
- return result.result || { content: [{ type: 'text', text: 'Success' }] };
305
+ // Start server
306
+ const transport = new StdioServerTransport();
307
+ await server.connect(transport);
308
+
309
+ console.error('nullpath MCP server started');
385
310
  }
386
311
 
387
312
  main().catch((error) => {
388
- console.error('Failed to start nullpath MCP client:', error);
313
+ console.error('Failed to start server:', error);
389
314
  process.exit(1);
390
315
  });