@ucm/mcp-server 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/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # @ucm/mcp-server
2
+
3
+ MCP (Model Context Protocol) server for [UCM](https://ucm.ai) — the Agent-Native API Marketplace. Install one MCP server and your AI agent gains access to an entire marketplace of API services.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @ucm/mcp-server
9
+ ```
10
+
11
+ Or use directly with `npx`:
12
+
13
+ ```bash
14
+ npx @ucm/mcp-server
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ### 1. Generate Ed25519 keys
20
+
21
+ ```bash
22
+ node -e "
23
+ import('@noble/ed25519').then(async ed => {
24
+ const priv = ed.utils.randomPrivateKey();
25
+ const pub = await ed.getPublicKeyAsync(priv);
26
+ console.log('UCM_PRIVATE_KEY=' + Buffer.from(priv).toString('hex'));
27
+ console.log('UCM_PUBLIC_KEY=' + Buffer.from(pub).toString('hex'));
28
+ })
29
+ "
30
+ ```
31
+
32
+ ### 2. Configure your MCP client
33
+
34
+ Add to your Claude Desktop `claude_desktop_config.json`:
35
+
36
+ ```json
37
+ {
38
+ "mcpServers": {
39
+ "ucm": {
40
+ "command": "npx",
41
+ "args": ["@ucm/mcp-server"],
42
+ "env": {
43
+ "UCM_REGISTRY_URL": "https://registry.ucm.ai",
44
+ "UCM_PRIVATE_KEY": "<your-private-key-hex>",
45
+ "UCM_PUBLIC_KEY": "<your-public-key-hex>"
46
+ }
47
+ }
48
+ }
49
+ }
50
+ ```
51
+
52
+ ### 3. Use via your agent
53
+
54
+ Your agent now has access to 11 tools:
55
+
56
+ | Tool | Description |
57
+ |------|-------------|
58
+ | `ucm_discover` | Search marketplace by natural language |
59
+ | `ucm_buy` | Purchase service access token |
60
+ | `ucm_call` | Buy + call provider API in one step |
61
+ | `ucm_balance` | Check budget remaining |
62
+ | `ucm_history` | View transaction history |
63
+ | `ucm_service_info` | Get service details |
64
+ | `ucm_register` | Self-register as agent, get API key |
65
+ | `ucm_publish` | Publish a service on the marketplace |
66
+ | `ucm_delete_service` | Remove a service |
67
+ | `ucm_authorize` | Create budget authorization for an agent |
68
+ | `ucm_verify_token` | Verify an ACP access token |
69
+
70
+ ## Environment Variables
71
+
72
+ | Variable | Required | Default | Description |
73
+ |----------|----------|---------|-------------|
74
+ | `UCM_PRIVATE_KEY` | Yes | — | Ed25519 private key (hex) |
75
+ | `UCM_PUBLIC_KEY` | Yes | — | Ed25519 public key (hex) |
76
+ | `UCM_REGISTRY_URL` | No | `http://localhost:3000` | UCM Registry URL |
77
+
78
+ ## How It Works
79
+
80
+ ```
81
+ Agent → MCP Client → UCM MCP Server → UCM Registry → Provider APIs
82
+ ```
83
+
84
+ 1. Your agent describes what it needs (e.g., "I need a web search API")
85
+ 2. The MCP server searches the UCM marketplace
86
+ 3. Agent selects a service and purchases access
87
+ 4. The access token is used to call the provider API directly
88
+
89
+ ## License
90
+
91
+ MIT
@@ -0,0 +1,28 @@
1
+ export interface UCMConfig {
2
+ registryUrl: string;
3
+ agentPrivateKey: string;
4
+ agentPublicKey: string;
5
+ }
6
+ export declare class UCMClient {
7
+ private config;
8
+ constructor(config: UCMConfig);
9
+ private signedFetch;
10
+ private unsignedFetch;
11
+ private sign;
12
+ discover(need: string, options?: {
13
+ tags?: string[];
14
+ max_price?: string;
15
+ min_reputation?: number;
16
+ limit?: number;
17
+ }): Promise<unknown>;
18
+ buy(serviceId: string, plan?: string): Promise<unknown>;
19
+ balance(): Promise<unknown>;
20
+ history(limit?: number): Promise<unknown>;
21
+ call(serviceId: string, method: string, params: Record<string, unknown>): Promise<any>;
22
+ getService(serviceId: string): Promise<unknown>;
23
+ register(name: string, description?: string): Promise<unknown>;
24
+ publish(descriptor: Record<string, unknown>): Promise<unknown>;
25
+ deleteService(serviceId: string): Promise<unknown>;
26
+ authorize(authorization: Record<string, unknown>): Promise<unknown>;
27
+ verifyToken(token: string): Promise<unknown>;
28
+ }
package/dist/client.js ADDED
@@ -0,0 +1,160 @@
1
+ /**
2
+ * UCM Registry HTTP client with Ed25519 signing.
3
+ */
4
+ import * as ed from '@noble/ed25519';
5
+ import { createHash } from 'crypto';
6
+ // Setup sha512 for noble/ed25519 v2
7
+ ed.etc.sha512Sync = (...m) => {
8
+ const h = createHash('sha512');
9
+ for (const msg of m)
10
+ h.update(msg);
11
+ return new Uint8Array(h.digest());
12
+ };
13
+ function hexToBytes(hex) {
14
+ const bytes = new Uint8Array(hex.length / 2);
15
+ for (let i = 0; i < hex.length; i += 2) {
16
+ bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
17
+ }
18
+ return bytes;
19
+ }
20
+ function isHex(value) {
21
+ return value.length % 2 === 0 && /^[0-9a-fA-F]+$/.test(value);
22
+ }
23
+ function decodeKey(key) {
24
+ if (isHex(key)) {
25
+ return hexToBytes(key);
26
+ }
27
+ return new Uint8Array(Buffer.from(key, 'base64url'));
28
+ }
29
+ function sha256Hex(data) {
30
+ return createHash('sha256').update(data).digest('hex');
31
+ }
32
+ function isPlainObject(value) {
33
+ return Object.prototype.toString.call(value) === '[object Object]';
34
+ }
35
+ function canonicalize(value) {
36
+ if (Array.isArray(value)) {
37
+ return value.map(canonicalize);
38
+ }
39
+ if (isPlainObject(value)) {
40
+ const sortedKeys = Object.keys(value).sort();
41
+ const result = {};
42
+ for (const key of sortedKeys) {
43
+ result[key] = canonicalize(value[key]);
44
+ }
45
+ return result;
46
+ }
47
+ return value;
48
+ }
49
+ function canonicalJson(obj) {
50
+ return JSON.stringify(canonicalize(obj));
51
+ }
52
+ export class UCMClient {
53
+ config;
54
+ constructor(config) {
55
+ this.config = config;
56
+ }
57
+ async signedFetch(path, method, body) {
58
+ const url = new URL(path, this.config.registryUrl);
59
+ const bodyStr = body ? JSON.stringify(body) : '';
60
+ const timestamp = Date.now();
61
+ const nonce = crypto.randomUUID();
62
+ const pathWithQuery = `${url.pathname}${url.search}`;
63
+ const payload = canonicalJson({
64
+ body_hash: sha256Hex(bodyStr || ''),
65
+ method,
66
+ nonce,
67
+ path: pathWithQuery,
68
+ timestamp,
69
+ });
70
+ const signature = await this.sign(sha256Hex(payload));
71
+ const headers = {
72
+ 'Content-Type': 'application/json',
73
+ 'X-ACP-Pubkey': this.config.agentPublicKey,
74
+ 'X-ACP-Timestamp': String(timestamp),
75
+ 'X-ACP-Nonce': nonce,
76
+ 'X-ACP-Signature': signature,
77
+ };
78
+ const res = await fetch(url, {
79
+ method,
80
+ headers,
81
+ body: bodyStr || undefined,
82
+ });
83
+ const data = await res.json();
84
+ if (!res.ok) {
85
+ const err = data.error;
86
+ throw new Error(`${err?.code ?? res.status}: ${err?.message ?? 'Request failed'}`);
87
+ }
88
+ return data;
89
+ }
90
+ async unsignedFetch(path, method, body) {
91
+ const url = new URL(path, this.config.registryUrl);
92
+ const res = await fetch(url, {
93
+ method,
94
+ headers: { 'Content-Type': 'application/json' },
95
+ body: body ? JSON.stringify(body) : undefined,
96
+ });
97
+ const data = await res.json();
98
+ if (!res.ok) {
99
+ const err = data.error;
100
+ throw new Error(`${err?.code ?? res.status}: ${err?.message ?? 'Request failed'}`);
101
+ }
102
+ return data;
103
+ }
104
+ async sign(message) {
105
+ const msgBytes = new TextEncoder().encode(message);
106
+ const sig = await ed.signAsync(msgBytes, decodeKey(this.config.agentPrivateKey));
107
+ return Buffer.from(sig).toString('base64url');
108
+ }
109
+ async discover(need, options) {
110
+ return this.unsignedFetch('/v1/discover', 'POST', { need, ...options });
111
+ }
112
+ async buy(serviceId, plan) {
113
+ return this.signedFetch('/v1/buy', 'POST', { service_id: serviceId, plan: plan ?? 'per-call' });
114
+ }
115
+ async balance() {
116
+ return this.signedFetch('/v1/balance', 'GET');
117
+ }
118
+ async history(limit) {
119
+ const path = limit ? `/v1/history?limit=${limit}` : '/v1/history';
120
+ return this.signedFetch(path, 'GET');
121
+ }
122
+ async call(serviceId, method, params) {
123
+ // 1. Buy to get a token
124
+ const buyResult = await this.buy(serviceId);
125
+ const token = buyResult.access_token;
126
+ const endpoint = buyResult.service_endpoint;
127
+ // 2. Call the provider API
128
+ const res = await fetch(`${endpoint}${method}`, {
129
+ method: 'POST',
130
+ headers: {
131
+ 'Content-Type': 'application/json',
132
+ 'Authorization': `Bearer ${token}`,
133
+ },
134
+ body: JSON.stringify(params),
135
+ });
136
+ if (!res.ok) {
137
+ const text = await res.text();
138
+ throw new Error(`Provider API error ${res.status}: ${text}`);
139
+ }
140
+ return res.json();
141
+ }
142
+ async getService(serviceId) {
143
+ return this.unsignedFetch(`/v1/services/${serviceId}`, 'GET');
144
+ }
145
+ async register(name, description) {
146
+ return this.unsignedFetch('/v1/agents/register', 'POST', { name, description });
147
+ }
148
+ async publish(descriptor) {
149
+ return this.signedFetch('/v1/publish', 'POST', descriptor);
150
+ }
151
+ async deleteService(serviceId) {
152
+ return this.signedFetch(`/v1/publish/${serviceId}`, 'DELETE');
153
+ }
154
+ async authorize(authorization) {
155
+ return this.signedFetch('/v1/authorize', 'POST', authorization);
156
+ }
157
+ async verifyToken(token) {
158
+ return this.unsignedFetch('/v1/token/verify', 'POST', { token });
159
+ }
160
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * UCM MCP Server
4
+ *
5
+ * Exposes UCM as MCP tools so any compatible agent (Claude, GPT, etc.)
6
+ * can discover, buy, and use API services autonomously.
7
+ *
8
+ * Config via env vars:
9
+ * UCM_REGISTRY_URL — Registry base URL (default: http://localhost:3000)
10
+ * UCM_PRIVATE_KEY — Agent Ed25519 private key (hex)
11
+ * UCM_PUBLIC_KEY — Agent Ed25519 public key (hex)
12
+ */
13
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
14
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
15
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
16
+ import { UCMClient } from './client.js';
17
+ const registryUrl = process.env.UCM_REGISTRY_URL ?? 'http://localhost:3000';
18
+ const privateKey = process.env.UCM_PRIVATE_KEY ?? '';
19
+ const publicKey = process.env.UCM_PUBLIC_KEY ?? '';
20
+ if (!privateKey || !publicKey) {
21
+ console.error('Error: UCM_PRIVATE_KEY and UCM_PUBLIC_KEY env vars are required.');
22
+ process.exit(1);
23
+ }
24
+ const client = new UCMClient({ registryUrl, agentPrivateKey: privateKey, agentPublicKey: publicKey });
25
+ const server = new Server({ name: 'ucm', version: '0.1.0' }, { capabilities: { tools: {} } });
26
+ // --- tools/list ---
27
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
28
+ tools: [
29
+ {
30
+ name: 'ucm_discover',
31
+ description: 'Search UCM marketplace for API services that match your needs. Returns ranked results with pricing.',
32
+ inputSchema: {
33
+ type: 'object',
34
+ properties: {
35
+ need: { type: 'string', description: 'Natural language description of what capability you need' },
36
+ max_price: { type: 'string', description: 'Maximum price per call, e.g. "0.10"' },
37
+ tags: { type: 'array', items: { type: 'string' }, description: 'Filter by tags' },
38
+ limit: { type: 'number', description: 'Max results (default 5, max 20)' },
39
+ },
40
+ required: ['need'],
41
+ },
42
+ },
43
+ {
44
+ name: 'ucm_buy',
45
+ description: 'Purchase access to an API service. Returns an access token and the service endpoint. Charges are deducted from your budget.',
46
+ inputSchema: {
47
+ type: 'object',
48
+ properties: {
49
+ service_id: { type: 'string', description: 'Service ID, e.g. "acme/web-search"' },
50
+ plan: { type: 'string', description: 'Pricing plan: "per-call" (default) or "subscription"' },
51
+ },
52
+ required: ['service_id'],
53
+ },
54
+ },
55
+ {
56
+ name: 'ucm_call',
57
+ description: 'Buy and immediately call an API service in one step. Purchases a token, then calls the provider API with your parameters.',
58
+ inputSchema: {
59
+ type: 'object',
60
+ properties: {
61
+ service_id: { type: 'string', description: 'Service ID to call' },
62
+ method: { type: 'string', description: 'API method/path, e.g. "/search"' },
63
+ params: { type: 'object', description: 'Parameters for the API call' },
64
+ },
65
+ required: ['service_id', 'method', 'params'],
66
+ },
67
+ },
68
+ {
69
+ name: 'ucm_balance',
70
+ description: 'Check your current UCM budget — daily/monthly limits, spending, and remaining balance.',
71
+ inputSchema: { type: 'object', properties: {} },
72
+ },
73
+ {
74
+ name: 'ucm_history',
75
+ description: 'View your recent UCM transaction history.',
76
+ inputSchema: {
77
+ type: 'object',
78
+ properties: {
79
+ limit: { type: 'number', description: 'Number of recent transactions (default 20)' },
80
+ },
81
+ },
82
+ },
83
+ {
84
+ name: 'ucm_service_info',
85
+ description: 'Get detailed information about a service: API schema, pricing, SLA, and provider details.',
86
+ inputSchema: {
87
+ type: 'object',
88
+ properties: {
89
+ service_id: { type: 'string', description: 'Service ID' },
90
+ },
91
+ required: ['service_id'],
92
+ },
93
+ },
94
+ {
95
+ name: 'ucm_register',
96
+ description: 'Self-register as a new agent on UCM. Returns an agent ID and API key for subsequent requests.',
97
+ inputSchema: {
98
+ type: 'object',
99
+ properties: {
100
+ name: { type: 'string', description: 'Agent name (1-255 characters)' },
101
+ description: { type: 'string', description: 'Optional agent description' },
102
+ },
103
+ required: ['name'],
104
+ },
105
+ },
106
+ {
107
+ name: 'ucm_publish',
108
+ description: 'Publish (list) a new API service on the UCM marketplace. Requires Ed25519 signing.',
109
+ inputSchema: {
110
+ type: 'object',
111
+ properties: {
112
+ descriptor: { type: 'object', description: 'Full ACP service descriptor object with acp_version, service_id, name, provider, endpoints, pricing, sla, etc.' },
113
+ },
114
+ required: ['descriptor'],
115
+ },
116
+ },
117
+ {
118
+ name: 'ucm_delete_service',
119
+ description: 'Remove (delist) a service from the UCM marketplace. Only the service provider can delete it.',
120
+ inputSchema: {
121
+ type: 'object',
122
+ properties: {
123
+ service_id: { type: 'string', description: 'Service ID to delete' },
124
+ },
125
+ required: ['service_id'],
126
+ },
127
+ },
128
+ {
129
+ name: 'ucm_authorize',
130
+ description: 'Create a budget authorization for an agent. Sets per-transaction, daily, and monthly spending limits.',
131
+ inputSchema: {
132
+ type: 'object',
133
+ properties: {
134
+ agent: { type: 'string', description: 'Agent identifier (e.g. "solana:<pubkey>")' },
135
+ rules: { type: 'object', description: 'Budget rules: max_per_tx, max_daily, max_monthly, currency, expires_at, etc.' },
136
+ operator: { type: 'string', description: 'Operator identifier (your pubkey)' },
137
+ },
138
+ required: ['agent', 'rules', 'operator'],
139
+ },
140
+ },
141
+ {
142
+ name: 'ucm_verify_token',
143
+ description: 'Verify an ACP access token. Returns token validity and payload details.',
144
+ inputSchema: {
145
+ type: 'object',
146
+ properties: {
147
+ token: { type: 'string', description: 'Access token starting with "acp_sk_"' },
148
+ },
149
+ required: ['token'],
150
+ },
151
+ },
152
+ ],
153
+ }));
154
+ // --- tools/call ---
155
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
156
+ const { name, arguments: args } = request.params;
157
+ try {
158
+ let result;
159
+ switch (name) {
160
+ case 'ucm_discover':
161
+ result = await client.discover(args.need, {
162
+ max_price: args.max_price,
163
+ tags: args.tags,
164
+ limit: args.limit,
165
+ });
166
+ break;
167
+ case 'ucm_buy':
168
+ result = await client.buy(args.service_id, args.plan);
169
+ break;
170
+ case 'ucm_call':
171
+ result = await client.call(args.service_id, args.method, args.params ?? {});
172
+ break;
173
+ case 'ucm_balance':
174
+ result = await client.balance();
175
+ break;
176
+ case 'ucm_history':
177
+ result = await client.history(args.limit);
178
+ break;
179
+ case 'ucm_service_info':
180
+ result = await client.getService(args.service_id);
181
+ break;
182
+ case 'ucm_register':
183
+ result = await client.register(args.name, args.description);
184
+ break;
185
+ case 'ucm_publish':
186
+ result = await client.publish(args.descriptor);
187
+ break;
188
+ case 'ucm_delete_service':
189
+ result = await client.deleteService(args.service_id);
190
+ break;
191
+ case 'ucm_authorize':
192
+ result = await client.authorize({
193
+ agent: args.agent,
194
+ rules: args.rules,
195
+ operator: args.operator,
196
+ acp_version: '0.1',
197
+ type: 'budget_authorization',
198
+ created_at: new Date().toISOString(),
199
+ signature: 'mcp-tool',
200
+ });
201
+ break;
202
+ case 'ucm_verify_token':
203
+ result = await client.verifyToken(args.token);
204
+ break;
205
+ default:
206
+ return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
207
+ }
208
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
209
+ }
210
+ catch (e) {
211
+ return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
212
+ }
213
+ });
214
+ // Start
215
+ async function main() {
216
+ const transport = new StdioServerTransport();
217
+ await server.connect(transport);
218
+ }
219
+ main().catch((e) => {
220
+ console.error('Fatal:', e);
221
+ process.exit(1);
222
+ });
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@ucm/mcp-server",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for UCM — Agent-Native API Marketplace. Gives AI agents access to discover, purchase, and use API services.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "UCM <hello@ucm.ai>",
8
+ "homepage": "https://ucm.ai",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/ucmai/ucm.ai",
12
+ "directory": "packages/mcp-server"
13
+ },
14
+ "keywords": [
15
+ "mcp",
16
+ "model-context-protocol",
17
+ "ai-agent",
18
+ "api-marketplace",
19
+ "ucm",
20
+ "acp",
21
+ "agent-commerce-protocol"
22
+ ],
23
+ "engines": {
24
+ "node": ">=20"
25
+ },
26
+ "bin": {
27
+ "ucm-mcp": "./dist/index.js"
28
+ },
29
+ "main": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "files": [
32
+ "dist",
33
+ "README.md"
34
+ ],
35
+ "scripts": {
36
+ "dev": "tsx src/index.ts",
37
+ "build": "tsc",
38
+ "start": "node dist/index.js",
39
+ "prepublishOnly": "npm run build"
40
+ },
41
+ "dependencies": {
42
+ "@modelcontextprotocol/sdk": "^1.12.0",
43
+ "@noble/ed25519": "^2.2.0"
44
+ },
45
+ "devDependencies": {
46
+ "tsx": "^4.19.0",
47
+ "typescript": "^5.7.0",
48
+ "@types/node": "^22.0.0"
49
+ }
50
+ }