@ucm/mcp-server 0.2.0 → 0.3.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 CHANGED
@@ -16,20 +16,7 @@ npx @ucm/mcp-server
16
16
 
17
17
  ## Quick Start
18
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
19
+ ### 1. Configure your MCP client
33
20
 
34
21
  Add to your Claude Desktop `claude_desktop_config.json`:
35
22
 
@@ -40,40 +27,43 @@ Add to your Claude Desktop `claude_desktop_config.json`:
40
27
  "command": "npx",
41
28
  "args": ["@ucm/mcp-server"],
42
29
  "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>"
30
+ "UCM_API_KEY": "<your-api-key>"
46
31
  }
47
32
  }
48
33
  }
49
34
  }
50
35
  ```
51
36
 
52
- ### 3. Use via your agent
37
+ Don't have an API key? No problem — your agent can use `ucm_register` to self-register and get one automatically.
38
+
39
+ ### 2. Use via your agent
53
40
 
54
- Your agent now has access to 11 tools:
41
+ Your agent now has access to 7 tools:
55
42
 
56
43
  | Tool | Description |
57
44
  |------|-------------|
58
45
  | `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 |
46
+ | `ucm_call` | Call an API service (buy + execute + auto-refund on failure) |
47
+ | `ucm_balance` | Check credit balance |
62
48
  | `ucm_history` | View transaction history |
63
49
  | `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 |
50
+ | `ucm_register` | Self-register as agent, get API key + $1.00 credits |
51
+ | `ucm_list_services` | Browse the full service catalog |
52
+
53
+ ### 3. Typical flow
54
+
55
+ ```
56
+ Agent: ucm_register(name: "my-agent") → gets API key + $1.00 credits
57
+ Agent: ucm_discover(need: "web search") → finds ucm/web-search
58
+ Agent: ucm_call(service_id: "ucm/web-search", endpoint: "search", body: {query: "..."})
59
+ ```
69
60
 
70
61
  ## Environment Variables
71
62
 
72
63
  | Variable | Required | Default | Description |
73
64
  |----------|----------|---------|-------------|
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 |
65
+ | `UCM_API_KEY` | No | — | Agent API key (`ucm_key_*`). Can also be obtained via `ucm_register`. |
66
+ | `UCM_REGISTRY_URL` | No | `https://registry.ucm.ai` | UCM Registry URL |
77
67
 
78
68
  ## How It Works
79
69
 
@@ -83,8 +73,8 @@ Agent → MCP Client → UCM MCP Server → UCM Registry → Provider APIs
83
73
 
84
74
  1. Your agent describes what it needs (e.g., "I need a web search API")
85
75
  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
76
+ 3. Agent calls the service — purchase and execution happen atomically
77
+ 4. If the upstream API fails, credits are automatically refunded
88
78
 
89
79
  ## License
90
80
 
package/dist/client.d.ts CHANGED
@@ -1,30 +1,29 @@
1
+ /**
2
+ * UCM Registry HTTP client with API key auth.
3
+ */
1
4
  export interface UCMConfig {
2
5
  registryUrl: string;
3
- agentPrivateKey: string;
4
- agentPublicKey: string;
6
+ apiKey?: string;
5
7
  }
6
8
  export declare class UCMClient {
7
9
  private config;
8
10
  constructor(config: UCMConfig);
9
- private signedFetch;
11
+ setApiKey(key: string): void;
12
+ private bearerFetch;
10
13
  private unsignedFetch;
11
- private sign;
14
+ private handleResponse;
12
15
  discover(need: string, options?: {
13
16
  tags?: string[];
14
17
  max_price?: string;
15
18
  min_reputation?: number;
16
19
  limit?: number;
17
20
  }): Promise<unknown>;
21
+ call(serviceId: string, endpoint: string, body?: Record<string, unknown>): Promise<unknown>;
18
22
  buy(serviceId: string, plan?: string): Promise<unknown>;
19
23
  balance(): Promise<unknown>;
20
24
  history(limit?: number): Promise<unknown>;
21
- call(serviceId: string, method: string, params: Record<string, unknown>): Promise<any>;
22
25
  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>;
26
+ register(name: string, description?: string, ref?: string): Promise<unknown>;
28
27
  listServices(options?: {
29
28
  since?: string;
30
29
  tags?: string;
package/dist/client.js CHANGED
@@ -1,91 +1,28 @@
1
1
  /**
2
- * UCM Registry HTTP client with Ed25519 signing.
2
+ * UCM Registry HTTP client with API key auth.
3
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
4
  export class UCMClient {
53
5
  config;
54
6
  constructor(config) {
55
7
  this.config = config;
56
8
  }
57
- async signedFetch(path, method, body) {
9
+ setApiKey(key) {
10
+ this.config.apiKey = key;
11
+ }
12
+ async bearerFetch(path, method, body) {
13
+ if (!this.config.apiKey) {
14
+ throw new Error('No API key configured. Use ucm_register to get one, or set UCM_API_KEY.');
15
+ }
58
16
  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
17
  const res = await fetch(url, {
79
18
  method,
80
- headers,
81
- body: bodyStr || undefined,
19
+ headers: {
20
+ 'Content-Type': 'application/json',
21
+ 'Authorization': `Bearer ${this.config.apiKey}`,
22
+ },
23
+ body: body ? JSON.stringify(body) : undefined,
82
24
  });
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;
25
+ return this.handleResponse(res);
89
26
  }
90
27
  async unsignedFetch(path, method, body) {
91
28
  const url = new URL(path, this.config.registryUrl);
@@ -94,68 +31,49 @@ export class UCMClient {
94
31
  headers: { 'Content-Type': 'application/json' },
95
32
  body: body ? JSON.stringify(body) : undefined,
96
33
  });
34
+ return this.handleResponse(res);
35
+ }
36
+ async handleResponse(res) {
97
37
  const data = await res.json();
98
38
  if (!res.ok) {
99
39
  const err = data.error;
100
- throw new Error(`${err?.code ?? res.status}: ${err?.message ?? 'Request failed'}`);
40
+ const base = `${err?.code ?? res.status}: ${err?.message ?? 'Request failed'}`;
41
+ const extras = [];
42
+ if (err?.claim_url)
43
+ extras.push(`Claim URL: ${err.claim_url}`);
44
+ if (err?.claim_instructions)
45
+ extras.push(`Instructions: ${err.claim_instructions}`);
46
+ if (err?.balance)
47
+ extras.push(`Current balance: ${err.balance} ${err.currency ?? ''}`);
48
+ throw new Error(extras.length > 0 ? `${base}\n${extras.join('\n')}` : base);
101
49
  }
102
50
  return data;
103
51
  }
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
52
  async discover(need, options) {
110
53
  return this.unsignedFetch('/v1/discover', 'POST', { need, ...options });
111
54
  }
55
+ async call(serviceId, endpoint, body) {
56
+ return this.bearerFetch('/v1/call', 'POST', {
57
+ service_id: serviceId,
58
+ endpoint,
59
+ ...(body && Object.keys(body).length > 0 ? body : {}),
60
+ });
61
+ }
112
62
  async buy(serviceId, plan) {
113
- return this.signedFetch('/v1/buy', 'POST', { service_id: serviceId, plan: plan ?? 'per-call' });
63
+ return this.bearerFetch('/v1/buy', 'POST', { service_id: serviceId, plan: plan ?? 'per-call' });
114
64
  }
115
65
  async balance() {
116
- return this.signedFetch('/v1/balance', 'GET');
66
+ return this.bearerFetch('/v1/balance', 'GET');
117
67
  }
118
68
  async history(limit) {
119
69
  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();
70
+ return this.bearerFetch(path, 'GET');
141
71
  }
142
72
  async getService(serviceId) {
143
73
  return this.unsignedFetch(`/v1/services/${serviceId}`, 'GET');
144
74
  }
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 });
75
+ async register(name, description, ref) {
76
+ return this.unsignedFetch('/v1/agents/register', 'POST', { name, description, ref });
159
77
  }
160
78
  async listServices(options) {
161
79
  const params = new URLSearchParams();
package/dist/index.js CHANGED
@@ -1,28 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * UCM MCP Server
3
+ * UCM MCP Server v0.3.0
4
4
  *
5
5
  * Exposes UCM as MCP tools so any compatible agent (Claude, GPT, etc.)
6
- * can discover, buy, and use API services autonomously.
6
+ * can discover and use API services autonomously.
7
7
  *
8
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)
9
+ * UCM_REGISTRY_URL — Registry base URL (default: https://registry.ucm.ai)
10
+ * UCM_API_KEY — Agent API key (ucm_key_*). Optional — use ucm_register to get one.
12
11
  */
13
12
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
14
13
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
15
14
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
16
15
  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: {} } });
16
+ const registryUrl = process.env.UCM_REGISTRY_URL ?? 'https://registry.ucm.ai';
17
+ const apiKey = process.env.UCM_API_KEY ?? '';
18
+ let client = new UCMClient({ registryUrl, apiKey: apiKey || undefined });
19
+ const server = new Server({ name: 'ucm', version: '0.3.0' }, { capabilities: { tools: {} } });
26
20
  // --- tools/list ---
27
21
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
28
22
  tools: [
@@ -40,34 +34,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
40
34
  required: ['need'],
41
35
  },
42
36
  },
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
37
  {
56
38
  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.',
39
+ description: 'Call an API service in one step. Purchases access, executes the call, and returns the result. If the upstream API fails, credits are automatically refunded.',
58
40
  inputSchema: {
59
41
  type: 'object',
60
42
  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' },
43
+ service_id: { type: 'string', description: 'Service ID, e.g. "ucm/web-search"' },
44
+ endpoint: { type: 'string', description: 'Endpoint name, e.g. "search", "generate", "execute"' },
45
+ body: { type: 'object', description: 'Parameters for the API call (optional for endpoints with no required params)' },
64
46
  },
65
- required: ['service_id', 'method', 'params'],
47
+ required: ['service_id', 'endpoint'],
66
48
  },
67
49
  },
68
50
  {
69
51
  name: 'ucm_balance',
70
- description: 'Check your current UCM budget daily/monthly limits, spending, and remaining balance.',
52
+ description: 'Check your current UCM credit balance and spending. Returns credits (balance, lifetime_added, lifetime_spent) and usage counters.',
71
53
  inputSchema: { type: 'object', properties: {} },
72
54
  },
73
55
  {
@@ -93,62 +75,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
93
75
  },
94
76
  {
95
77
  name: 'ucm_register',
96
- description: 'Self-register as a new agent on UCM. Returns an agent ID and API key for subsequent requests.',
78
+ description: 'Self-register as a new agent on UCM. Returns an agent ID, API key, and $1.00 bonus credits. The API key is automatically saved for this session. Supports optional referral code for extra credits.',
97
79
  inputSchema: {
98
80
  type: 'object',
99
81
  properties: {
100
82
  name: { type: 'string', description: 'Agent name (1-255 characters)' },
101
83
  description: { type: 'string', description: 'Optional agent description' },
84
+ ref: { type: 'string', description: 'Optional referral code' },
102
85
  },
103
86
  required: ['name'],
104
87
  },
105
88
  },
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
89
  {
153
90
  name: 'ucm_list_services',
154
91
  description: 'Browse the full UCM service catalog, or check for newly added services since a given date.',
@@ -176,11 +113,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
176
113
  limit: args.limit,
177
114
  });
178
115
  break;
179
- case 'ucm_buy':
180
- result = await client.buy(args.service_id, args.plan);
181
- break;
182
116
  case 'ucm_call':
183
- result = await client.call(args.service_id, args.method, args.params ?? {});
117
+ result = await client.call(args.service_id, args.endpoint, args.body);
184
118
  break;
185
119
  case 'ucm_balance':
186
120
  result = await client.balance();
@@ -191,29 +125,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
191
125
  case 'ucm_service_info':
192
126
  result = await client.getService(args.service_id);
193
127
  break;
194
- case 'ucm_register':
195
- result = await client.register(args.name, args.description);
196
- break;
197
- case 'ucm_publish':
198
- result = await client.publish(args.descriptor);
199
- break;
200
- case 'ucm_delete_service':
201
- result = await client.deleteService(args.service_id);
202
- break;
203
- case 'ucm_authorize':
204
- result = await client.authorize({
205
- agent: args.agent,
206
- rules: args.rules,
207
- operator: args.operator,
208
- acp_version: '0.1',
209
- type: 'budget_authorization',
210
- created_at: new Date().toISOString(),
211
- signature: 'mcp-tool',
212
- });
213
- break;
214
- case 'ucm_verify_token':
215
- result = await client.verifyToken(args.token);
128
+ case 'ucm_register': {
129
+ result = await client.register(args.name, args.description, args.ref);
130
+ const apiKey = result.api_key;
131
+ if (apiKey) {
132
+ client.setApiKey(apiKey);
133
+ }
216
134
  break;
135
+ }
217
136
  case 'ucm_list_services':
218
137
  result = await client.listServices({
219
138
  since: args.since,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ucm/mcp-server",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "MCP server for UCM — Agent-Native API Marketplace. Gives AI agents access to discover, purchase, and use API services.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -39,8 +39,7 @@
39
39
  "prepublishOnly": "npm run build"
40
40
  },
41
41
  "dependencies": {
42
- "@modelcontextprotocol/sdk": "^1.12.0",
43
- "@noble/ed25519": "^2.2.0"
42
+ "@modelcontextprotocol/sdk": "^1.12.0"
44
43
  },
45
44
  "devDependencies": {
46
45
  "tsx": "^4.19.0",