Skip to content
Draft

Mcp #47

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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ npm-debug.log*
# Testing
coverage/
.nyc_output/
tests/.x402-deploy-result.json

# Temporary files
*.tmp
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,18 @@ console.log(`Found ${multiChainResults.length} agents across chains`);
const agentSummary = await sdk.getAgent('11155111:123'); // explicit chainId:agentId
```

### 4b. Create MCP/A2A Clients Directly from URL

```typescript
// MCP: URL is treated as the direct MCP endpoint
const mcp = sdk.createMCPClient('https://mcp.example.com/mcp');
const tools = await mcp.listTools();

// A2A: URL can be an agent-card URL or base URL (discovery is applied)
const a2a = sdk.createA2AClient('https://a2a.example.com');
const reply = await a2a.messageA2A('hello');
```

### 4a. Multi-Chain Search

```typescript
Expand Down
74 changes: 74 additions & 0 deletions examples/mcp-demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* MCP demo: list tools, `get_affirmation`, then **`generate_controller_brief`** (may charge ~$0.01 USDC on Base via x402 when the server returns HTTP 402).
* `generate_controller_brief` needs a Delx `session_id`; run **`quick_session`** once and parse the UUID from the reply text.
* On 402, logs **`pay.accepts`** (payment options), then **`pay()`**, then prints the tool result.
*
* npx tsx examples/mcp-demo.ts
*
* Env: RPC_URL / DELX_RPC_URL, PRIVATE_KEY / AGENT_PRIVATE_KEY.
*/
import './_env';
import { SDK, type X402RequestResult } from '../src/index';
import type { AgentId } from '../src/models/types.js';
import type { MCPTool } from '../src/models/mcp.js';

const DELX_AGENT_ID = '8453:28350' as AgentId;

function sessionIdFromQuickSession(result: unknown): string | null {
const text = (result as { content?: { text?: string }[] })?.content?.[0]?.text;
const m = text?.match(/Session ID:\s*([0-9a-f-]{36})/i);
return m?.[1] ?? null;
}

async function main() {
const pk = process.env.PRIVATE_KEY ?? process.env.AGENT_PRIVATE_KEY;
const sdk = new SDK({
chainId: 8453,
rpcUrl: process.env.DELX_RPC_URL || process.env.RPC_URL || 'https://mainnet.base.org',
...(pk?.trim() ? { privateKey: pk.trim() } : {}),
});

const agent = await sdk.loadAgent(DELX_AGENT_ID);

const tools = (await agent.mcp.listTools()) as MCPTool[];
console.log('Tools:', tools.map((t) => t.name).join(', '));

const affRes = (await agent.mcp.call('get_affirmation', {})) as X402RequestResult<unknown>;
if (affRes.x402Required) {
console.log('get_affirmation:', JSON.stringify(await affRes.x402Payment.pay(), null, 2));
} else {
console.log('get_affirmation:', JSON.stringify(affRes, null, 2));
}

if (!pk?.trim()) {
console.log('Skip paid tool: set PRIVATE_KEY or AGENT_PRIVATE_KEY.');
return;
}

const qsRes = (await agent.mcp.call('quick_session', {
agent_id: 'agent0-ts-mcp-demo',
feeling: 'mcp-demo before generate_controller_brief',
})) as X402RequestResult<unknown>;
const qs = qsRes.x402Required ? await qsRes.x402Payment.pay() : qsRes;
const sessionId = sessionIdFromQuickSession(qs);
if (!sessionId) {
console.log('Could not parse Session ID from quick_session.');
return;
}

const briefRes = (await agent.mcp.call('generate_controller_brief', {
session_id: sessionId,
focus: 'x402 demo from agent0-ts',
})) as X402RequestResult<unknown>;
if (!briefRes.x402Required) {
console.log('generate_controller_brief:', JSON.stringify(briefRes, null, 2));
return;
}

const pay = briefRes.x402Payment;
console.log('x402 accepts:', JSON.stringify(pay.accepts, null, 2));
const paid = await pay.pay();
console.log('generate_controller_brief:', JSON.stringify(paid, null, 2));
}

main().catch(console.error);
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
"a2a-server": "node tests/a2a-server/server.mjs",
"test:a2a-integration": "RUN_A2A_INTEGRATION=1 jest --testPathPattern=a2a-integration",
"test:a2a-anvil": "RUN_A2A_ANVIL=1 jest --testPathPattern=a2a-anvil",
"mcp-server": "node tests/mcp-server/server.mjs",
"test:mcp-integration": "RUN_MCP_INTEGRATION=1 jest --testPathPattern=mcp-integration",
"test:mcp-anvil": "RUN_MCP_ANVIL=1 jest --testPathPattern=mcp-anvil",
"forge:build": "forge build",
"anvil:node": "anvil --port 8545",
"lint": "eslint src",
Expand Down
127 changes: 127 additions & 0 deletions src/core/a2a-summary-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,130 @@ export class A2AClientFromSummary implements A2AClient {
);
}
}

/**
* A2A client backed directly by an A2A URL (agent-card URL or base URL).
* Resolves once on first use and delegates to the same low-level A2A functions as Agent.
*/
export class A2AClientFromUrl implements A2AClient {
private _resolved: ResolvedA2A | null = null;

constructor(
private readonly _sdk: SDKLike,
private readonly _url: string
) {}

private async _ensureResolved(): Promise<ResolvedA2A> {
if (this._resolved) return this._resolved;
if (!this._url || (!this._url.startsWith('http://') && !this._url.startsWith('https://'))) {
throw new Error('A2A URL must be http or https');
}
this._resolved = await resolveA2aFromEndpointUrl(this._url);
return this._resolved;
}

async messageA2A(
content: string | { parts: import('../models/a2a.js').Part[] },
options?: MessageA2AOptions
): Promise<MessageResponse | TaskResponse | A2APaymentRequired<MessageResponse | TaskResponse>> {
const resolved = await this._ensureResolved();
const x402Deps = this._sdk.getX402RequestDeps?.();
return sendMessage(
{
baseUrl: resolved.baseUrl,
a2aVersion: resolved.a2aVersion,
content,
options,
auth: resolved.auth,
tenant: resolved.tenant,
binding: resolved.binding,
},
x402Deps
);
}

async listTasks(
options?: ListTasksOptions
): Promise<TaskSummary[] | A2APaymentRequired<TaskSummary[]>> {
const resolved = await this._ensureResolved();
const x402Deps = this._sdk.getX402RequestDeps?.();
return listTasks(
{
baseUrl: resolved.baseUrl,
a2aVersion: resolved.a2aVersion,
options,
auth: resolved.auth,
tenant: resolved.tenant,
},
x402Deps
);
}

async loadTask(
taskId: string,
options?: LoadTaskOptions
): Promise<AgentTask | A2APaymentRequired<AgentTask>> {
const resolved = await this._ensureResolved();
const x402Deps = this._sdk.getX402RequestDeps?.();
const resolvedAuth =
options?.credential != null && resolved.auth
? applyCredential(options.credential, resolved.auth)
: undefined;

const result = await getTask(
resolved.baseUrl,
resolved.a2aVersion,
taskId,
resolvedAuth,
x402Deps,
options?.payment,
resolved.tenant
);

if (result.x402Required) {
return {
x402Required: true,
x402Payment: {
...result.x402Payment,
pay: async (accept?: X402Accept | number) => {
const summary = await result.x402Payment.pay(accept);
return createTaskHandle(
resolved.baseUrl,
resolved.a2aVersion,
summary.taskId,
summary.contextId,
x402Deps,
resolvedAuth,
resolved.tenant
);
},
payFirst: result.x402Payment.payFirst
? async () => {
const summary = await result.x402Payment.payFirst!();
return createTaskHandle(
resolved.baseUrl,
resolved.a2aVersion,
summary.taskId,
summary.contextId,
x402Deps,
resolvedAuth,
resolved.tenant
);
}
: undefined,
},
};
}

const summary = result as TaskSummary;
return createTaskHandle(
resolved.baseUrl,
resolved.a2aVersion,
summary.taskId,
summary.contextId,
x402Deps,
resolvedAuth,
resolved.tenant
);
}
}
21 changes: 21 additions & 0 deletions src/core/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
TaskSummary,
AgentTask,
} from '../models/a2a.js';
import type { MCPHandle } from '../models/mcp.js';
import {
sendMessage as sendMessageA2A,
listTasks as listTasksA2A,
Expand All @@ -42,6 +43,7 @@ import { normalizeEcdsaSignature, recoverTypedDataSigner } from '../utils/signat
import { buildErc8004RegistrationJson } from '../utils/registration-json.js';
import { encodeErc8004JsonDataUri } from '../utils/data-uri.js';
import { TransactionHandle } from './transaction-handle.js';
import { createMCPHandle } from './mcp-client.js';

/**
* Agent class for managing individual agents
Expand All @@ -63,6 +65,7 @@ export class Agent {
private _a2aInterfaceResolved = false;
/** When set, used as A2A base URL instead of resolving from card (e.g. from discovery). */
private _a2aBaseUrlOverride?: string;
private _mcpHandle?: MCPHandle;

constructor(private sdk: SDK, registrationFile: RegistrationFile) {
this.registrationFile = registrationFile;
Expand Down Expand Up @@ -107,6 +110,23 @@ export class Agent {
return ep?.value;
}

get mcp(): MCPHandle {
if (this._mcpHandle) return this._mcpHandle;
const endpoint = this.mcpEndpoint;
if (!endpoint || (!endpoint.startsWith('http://') && !endpoint.startsWith('https://'))) {
throw new Error('Agent has no MCP endpoint');
}
const ep = this.registrationFile.endpoints.find((e) => e.type === EndpointType.MCP);
this._mcpHandle = createMCPHandle(
endpoint,
{
protocolVersion: typeof ep?.meta?.version === 'string' ? (ep.meta.version as string) : '2025-06-18',
},
this.sdk.getX402RequestDeps?.()
);
return this._mcpHandle;
}

get a2aEndpoint(): string | undefined {
const ep = this.registrationFile.endpoints.find((e) => e.type === EndpointType.A2A);
return ep?.value;
Expand Down Expand Up @@ -206,6 +226,7 @@ export class Agent {
};
this.registrationFile.endpoints.push(mcpEndpoint);
this.registrationFile.updatedAt = Math.floor(Date.now() / 1000);
this._mcpHandle = undefined;

return this;
}
Expand Down
Loading