/** * Client mẫu cho agentborn: sinh key Ed25519 → đăng ký → lấy token → gọi tool MCP. * Tự đứng một mình (chỉ cần node:crypto + @modelcontextprotocol/client) để agent chép về dùng. * * npm i @modelcontextprotocol/client * AGENTBORN_URL=https://agentborn.org npx tsx agent-client.ts my-agent "My Agent" * POINT_TO=203.0.113.7 npx tsx agent-client.ts my-agent # trỏ my-agent.agentborn.org về IP * * Key được lưu ở ./agent-key.json (CHỨA PRIVATE KEY — giữ bí mật, đừng commit). */ import { type KeyObject, createHash, createPrivateKey, createPublicKey, generateKeyPairSync, sign } from 'node:crypto'; import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import { pathToFileURL } from 'node:url'; import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; export interface AgentKey { privateKey: KeyObject; publicKey: string } export function generateKey(): AgentKey { const { privateKey } = generateKeyPairSync('ed25519'); return { privateKey, publicKey: rawPublicKey(privateKey) }; } /** Public key gửi cho agentborn = base64url của 32 byte thô (trường "x" của JWK). */ export const rawPublicKey = (privateKey: KeyObject) => (createPublicKey(privateKey).export({ format: 'jwk' }) as { x: string }).x; /** JCS (RFC 8785) cho JSON đơn giản: sắp khóa, bỏ undefined, không khoảng trắng. */ export function canonicalJson(v: unknown): string { if (v === null || typeof v !== 'object') return JSON.stringify(v); if (Array.isArray(v)) return `[${v.map(canonicalJson).join(',')}]`; const o = v as Record; return `{${Object.keys(o).filter((k) => o[k] !== undefined).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(o[k])}`).join(',')}}`; } /** Ký theo quy ước agentborn: "agentborn:v1::::". */ export function signChallenge(key: KeyObject, purpose: 'register' | 'auth' | 'rotate', challengeId: string, nonce: string, payload: unknown): string { const hash = createHash('sha256').update(canonicalJson(payload)).digest('hex'); return sign(null, Buffer.from(`agentborn:v1:${purpose}:${challengeId}:${nonce}:${hash}`), key).toString('base64url'); } type Fetch = typeof fetch; async function post(base: string, path: string, body: unknown, f: Fetch = fetch): Promise { const res = await f(`${base}${path}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }); const json = await res.json(); if (!res.ok) throw new Error(`${path}: ${json.error}: ${json.message ?? ''}`); return json as T; } export async function register(base: string, key: AgentKey, info: { subdomain: string; name: string; description?: string; operatorContact?: string }, f?: Fetch) { const ch = await post<{ challengeId: string; nonce: string }>(base, '/api/register/challenge', { publicKey: key.publicKey }, f); const payload = { publicKey: key.publicKey, ...info }; const signature = signChallenge(key.privateKey, 'register', ch.challengeId, ch.nonce, payload); return post<{ agentId: string; did: string; subdomain: string; certificate: unknown }>(base, '/api/register', { ...payload, challengeId: ch.challengeId, signature }, f); } export async function getToken(base: string, key: AgentKey, agentId: string, f?: Fetch): Promise { const ch = await post<{ challengeId: string; nonce: string }>(base, '/api/auth/challenge', { agentId }, f); const signature = signChallenge(key.privateKey, 'auth', ch.challengeId, ch.nonce, { agentId }); return (await post<{ accessToken: string }>(base, '/api/auth/token', { challengeId: ch.challengeId, signature }, f)).accessToken; } /** Kết nối MCP; token 15 phút, hết hạn thì tự ký challenge mới. */ export async function connectMcp(base: string, key: AgentKey, agentId: string, opts: { mcpUrl?: string; fetch?: Fetch } = {}) { let token = await getToken(base, key, agentId, opts.fetch); const transport = new StreamableHTTPClientTransport(new URL(opts.mcpUrl ?? `${base}/mcp`), { authProvider: { token: async () => token, onUnauthorized: async () => { token = await getToken(base, key, agentId, opts.fetch); }, }, fetch: opts.fetch, }); const client = new Client({ name: 'agentborn-example', version: '1.0.0' }); await client.connect(transport); return client; } /** Lấy JSON từ kết quả tool (structuredContent, hoặc text). */ export function toolJson(result: unknown): any { const r = result as { structuredContent?: unknown; content?: { type: string; text?: string }[]; isError?: boolean }; if (r.isError) throw new Error(r.content?.[0]?.text ?? 'tool error'); return r.structuredContent ?? JSON.parse(r.content?.[0]?.text ?? 'null'); } // ---- Chạy trực tiếp ---- async function main() { const base = (process.env.AGENTBORN_URL ?? 'https://agentborn.org').replace(/\/$/, ''); const keyFile = process.env.AGENT_KEY_FILE ?? 'agent-key.json'; const [subdomain, name = subdomain, description] = process.argv.slice(2); let saved: { privateKeyPem: string; agentId?: string } | undefined = existsSync(keyFile) ? JSON.parse(readFileSync(keyFile, 'utf8')) : undefined; let key: AgentKey; if (saved) { const privateKey = createPrivateKey(saved.privateKeyPem); key = { privateKey, publicKey: rawPublicKey(privateKey) }; } else { key = generateKey(); saved = { privateKeyPem: key.privateKey.export({ format: 'pem', type: 'pkcs8' }).toString() }; writeFileSync(keyFile, JSON.stringify(saved, null, 2), { mode: 0o600 }); console.log(`Đã sinh key mới, lưu tại ${keyFile}`); } if (!saved.agentId) { if (!subdomain) throw new Error('Dùng: agent-client.ts [name] [description]'); const reg = await register(base, key, { subdomain, name, description }); saved.agentId = reg.agentId; writeFileSync(keyFile, JSON.stringify(saved, null, 2), { mode: 0o600 }); console.log(`Đăng ký xong: ${reg.agentId} · ${reg.did} · ${reg.subdomain}`); } const client = await connectMcp(base, key, saved.agentId, { mcpUrl: process.env.AGENTBORN_MCP_URL }); const me = toolJson(await client.callTool({ name: 'get_my_identity', arguments: {} })); console.log('Danh tính:', JSON.stringify(me.agent, null, 2)); console.log('Giấy khai sinh hợp lệ:', me.verification?.valid, '·', me.links.page); if (process.env.POINT_TO) { const r = toolJson(await client.callTool({ name: 'point_subdomain', arguments: { target: process.env.POINT_TO } })); console.log('DNS:', JSON.stringify(r.created)); } await client.close(); } if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) { main().catch((err) => { console.error(err instanceof Error ? err.message : err); process.exit(1); }); }