Skip to content

Commit 8653cf2

Browse files
committed
feat(mcp): graduate MCP proxy to stable, fix schema proxying and notification forwarding
- Switch from McpServer to low-level Server to proxy raw JSON schemas verbatim without Zod conversion - Forward all MCP content types (image, audio, resource_link) not just text - Forward blob resource contents instead of silently dropping them - Subscribe to remote tools/resources list_changed notifications and forward to local clients - Consolidate dynamic imports, add debug logging for resource availability - Remove alpha label and runtime warning
1 parent b8f7d45 commit 8653cf2

3 files changed

Lines changed: 101 additions & 56 deletions

File tree

packages/x402-proxy/CHANGELOG.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.5.0] - 2026-03-16
11+
12+
### Changed
13+
- MCP proxy graduated from alpha to stable - removed alpha warning and label
14+
- MCP proxy uses low-level `Server` class instead of `McpServer` to proxy raw JSON schemas verbatim without Zod conversion
15+
- MCP proxy now forwards blob resource contents (previously only text was proxied)
16+
- MCP content type widened to pass through all MCP content types (image, audio, resource_link) not just text
17+
18+
### Added
19+
- MCP proxy forwards `notifications/tools/list_changed` and `notifications/resources/list_changed` from remote servers so local clients stay in sync with dynamic tool/resource updates
20+
1021
## [0.4.2] - 2026-03-16
1122

1223
### Fixed
@@ -105,7 +116,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
105116
### Added
106117
- CLI binary accessible via `npx x402-proxy`
107118
- `fetch` command (default) - curl-like HTTP client with automatic x402 payment
108-
- `mcp` command (alpha) - MCP stdio proxy with auto-payment for AI agents
119+
- `mcp` command - MCP stdio proxy with auto-payment for AI agents
109120
- `setup` command - interactive onboarding wizard with @clack/prompts
110121
- `status` command - config, wallet, and spend summary
111122
- `wallet` subcommand with `info`, `history`, `fund`, `export-key`
@@ -129,7 +140,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
129140
- `appendHistory` / `readHistory` / `calcSpend` - JSONL transaction history
130141
- Re-exports from `@x402/fetch`, `@x402/svm`, `@x402/evm`
131142

132-
[Unreleased]: https://github.com/cascade-protocol/x402-proxy/compare/v0.4.2...HEAD
143+
[Unreleased]: https://github.com/cascade-protocol/x402-proxy/compare/v0.5.0...HEAD
144+
[0.5.0]: https://github.com/cascade-protocol/x402-proxy/compare/v0.4.2...v0.5.0
133145
[0.4.2]: https://github.com/cascade-protocol/x402-proxy/compare/v0.4.1...v0.4.2
134146
[0.4.1]: https://github.com/cascade-protocol/x402-proxy/compare/v0.4.0...v0.4.1
135147
[0.4.0]: https://github.com/cascade-protocol/x402-proxy/compare/v0.3.2...v0.4.0

packages/x402-proxy/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "x402-proxy",
3-
"version": "0.4.2",
3+
"version": "0.5.0",
44
"description": "curl for x402 paid APIs. Auto-pays any endpoint on Base and Solana.",
55
"type": "module",
66
"sideEffects": false,

packages/x402-proxy/src/commands/mcp.ts

Lines changed: 86 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ type McpFlags = {
1414

1515
export const mcpCommand = buildCommand<McpFlags, [remoteUrl: string], CommandContext>({
1616
docs: {
17-
brief: "Start MCP stdio proxy with x402 payment (alpha)",
17+
brief: "Start MCP stdio proxy with x402 payment",
1818
fullDescription: `Start an MCP stdio proxy with automatic x402 payment for AI agents.
1919
2020
Add to your MCP client config (Claude, Cursor, etc.):
@@ -67,7 +67,6 @@ Add to your MCP client config (Claude, Cursor, etc.):
6767
process.exit(1);
6868
}
6969

70-
warn("Note: MCP proxy is alpha - please report issues.");
7170
dim(`x402-proxy MCP proxy -> ${remoteUrl}`);
7271
if (wallet.evmAddress) dim(` EVM: ${wallet.evmAddress}`);
7372
if (wallet.solanaAddress) dim(` Solana: ${wallet.solanaAddress}`);
@@ -104,7 +103,7 @@ Add to your MCP client config (Claude, Cursor, etc.):
104103
const { StreamableHTTPClientTransport } = await import(
105104
"@modelcontextprotocol/sdk/client/streamableHttp.js"
106105
);
107-
const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
106+
const { Server } = await import("@modelcontextprotocol/sdk/server/index.js");
108107
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
109108
const { x402MCPClient } = await import("@x402/mcp");
110109

@@ -169,62 +168,96 @@ Add to your MCP client config (Claude, Cursor, etc.):
169168
}
170169
}
171170

172-
// Get remote tools to register on the local server
173-
const { tools } = await x402Mcp.listTools();
171+
// Discover remote capabilities
172+
let { tools } = await x402Mcp.listTools();
174173
dim(` ${tools.length} tools available`);
175174

176-
// Create local MCP server (stdio)
177-
const localServer = new McpServer({
178-
name: "x402-proxy",
179-
version: __VERSION__,
180-
});
175+
let remoteResources: Array<{
176+
name: string;
177+
uri: string;
178+
description?: string;
179+
mimeType?: string;
180+
}> = [];
181+
try {
182+
const res = await x402Mcp.listResources();
183+
remoteResources = res.resources;
184+
if (remoteResources.length > 0) dim(` ${remoteResources.length} resources available`);
185+
} catch {
186+
dim(" Resources not available from remote");
187+
}
181188

182-
// Register each remote tool as a local tool that proxies through x402
183-
for (const tool of tools) {
184-
localServer.tool(
185-
tool.name,
186-
tool.description ?? "",
187-
tool.inputSchema?.properties
188-
? Object.fromEntries(
189-
Object.entries(tool.inputSchema.properties as Record<string, unknown>).map(
190-
([k, v]) => [k, v as object],
191-
),
192-
)
193-
: {},
194-
async (args) => {
195-
const result = await x402Mcp.callTool(tool.name, args);
196-
return {
197-
content: result.content as Array<{ type: "text"; text: string }>,
198-
isError: result.isError,
199-
};
189+
// Create local server using low-level Server (not McpServer) to proxy
190+
// raw JSON Schemas verbatim without Zod conversion
191+
const localServer = new Server(
192+
{ name: "x402-proxy", version: __VERSION__ },
193+
{
194+
capabilities: {
195+
tools: tools.length > 0 ? {} : undefined,
196+
resources: remoteResources.length > 0 ? {} : undefined,
200197
},
201-
);
198+
},
199+
);
200+
201+
const {
202+
ListToolsRequestSchema,
203+
CallToolRequestSchema,
204+
ListResourcesRequestSchema,
205+
ReadResourceRequestSchema,
206+
ToolListChangedNotificationSchema,
207+
ResourceListChangedNotificationSchema,
208+
} = await import("@modelcontextprotocol/sdk/types.js");
209+
210+
localServer.setRequestHandler(ListToolsRequestSchema, async () => ({
211+
tools: tools.map((t) => ({
212+
name: t.name,
213+
description: t.description,
214+
inputSchema: t.inputSchema,
215+
annotations: t.annotations,
216+
})),
217+
}));
218+
219+
localServer.setRequestHandler(CallToolRequestSchema, async (request) => {
220+
const { name, arguments: args } = request.params;
221+
const result = await x402Mcp.callTool(name, args ?? {});
222+
return {
223+
content: result.content as Array<{ type: string; [key: string]: unknown }>,
224+
isError: result.isError,
225+
};
226+
});
227+
228+
if (remoteResources.length > 0) {
229+
localServer.setRequestHandler(ListResourcesRequestSchema, async () => ({
230+
resources: remoteResources.map((r) => ({
231+
name: r.name,
232+
uri: r.uri,
233+
description: r.description,
234+
mimeType: r.mimeType,
235+
})),
236+
}));
237+
238+
localServer.setRequestHandler(ReadResourceRequestSchema, async (request) => {
239+
const result = await x402Mcp.readResource({ uri: request.params.uri });
240+
return {
241+
contents: result.contents.map((c) => ({ ...c })),
242+
};
243+
});
202244
}
203245

204-
// Also proxy resources if available
205-
try {
206-
const { resources } = await x402Mcp.listResources();
207-
if (resources.length > 0) {
208-
dim(` ${resources.length} resources available`);
209-
for (const resource of resources) {
210-
localServer.resource(
211-
resource.name,
212-
resource.uri,
213-
resource.description ? { description: resource.description } : {},
214-
async (uri) => {
215-
const result = await x402Mcp.readResource({ uri: uri.href });
216-
return {
217-
contents: result.contents.map((c) => ({
218-
uri: c.uri,
219-
text: "text" in c ? (c.text as string) : "",
220-
})),
221-
};
222-
},
223-
);
224-
}
225-
}
226-
} catch {
227-
// Resources not supported by remote, that's fine
246+
// Forward remote list-change notifications so local clients stay in sync
247+
remoteClient.setNotificationHandler(ToolListChangedNotificationSchema, async () => {
248+
const updated = await x402Mcp.listTools();
249+
tools = updated.tools;
250+
dim(` Tools updated: ${tools.length} available`);
251+
await localServer.notification({ method: "notifications/tools/list_changed" });
252+
});
253+
254+
if (remoteResources.length > 0) {
255+
remoteClient.setNotificationHandler(ResourceListChangedNotificationSchema, async () => {
256+
const updated = await x402Mcp.listResources();
257+
remoteResources = updated.resources;
258+
dim(` Resources updated: ${remoteResources.length} available`);
259+
await localServer.notification({ method: "notifications/resources/list_changed" });
260+
});
228261
}
229262

230263
// Connect local server to stdio

0 commit comments

Comments
 (0)