ugarapi-mcp-server 1.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/DEPLOYMENT_GUIDE.md +343 -0
- package/GITHUB_SETUP_GUIDE.md +189 -0
- package/MARKETING_STRATEGY.md +418 -0
- package/OPENNODE_DEPLOY_GUIDE.md +191 -0
- package/OPENNODE_MANUAL_INTEGRATION.md +185 -0
- package/README.md +165 -0
- package/ai_service_business_plan.md +200 -0
- package/claude_desktop_config.json +14 -0
- package/main-v1.2-opennode.cpython-312.pyc +0 -0
- package/main-v1.2-opennode.py +588 -0
- package/main.py +395 -0
- package/package.json +29 -0
- package/requirements.txt +9 -0
- package/test_agent_simulator.py +296 -0
- package/ugarapi-banner.html +441 -0
- package/ugarapi-complete-package.zip +0 -0
- package/ugarapi-mcp-server.js +335 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* UgarAPI MCP Server
|
|
5
|
+
*
|
|
6
|
+
* Exposes UgarAPI services as MCP tools for AI agents.
|
|
7
|
+
* Handles Bitcoin Lightning payments automatically.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node ugarapi-mcp-server.js
|
|
11
|
+
*
|
|
12
|
+
* Configuration via environment variables:
|
|
13
|
+
* UGARAPI_BASE_URL - defaults to https://ugarapi.com
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
17
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
18
|
+
import {
|
|
19
|
+
CallToolRequestSchema,
|
|
20
|
+
ListToolsRequestSchema,
|
|
21
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
22
|
+
|
|
23
|
+
const UGARAPI_BASE_URL = process.env.UGARAPI_BASE_URL || "https://ugarapi.com";
|
|
24
|
+
|
|
25
|
+
// Service prices in sats
|
|
26
|
+
const PRICES = {
|
|
27
|
+
web_extraction: 1000,
|
|
28
|
+
document_timestamp: 5000,
|
|
29
|
+
api_aggregator: 200,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// In-memory payment cache (in production, use persistent storage)
|
|
33
|
+
const paymentCache = new Map();
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create a Lightning invoice for a service
|
|
37
|
+
*/
|
|
38
|
+
async function createInvoice(service, idempotencyKey) {
|
|
39
|
+
const response = await fetch(`${UGARAPI_BASE_URL}/api/v1/payment/create`, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: { "Content-Type": "application/json" },
|
|
42
|
+
body: JSON.stringify({
|
|
43
|
+
service,
|
|
44
|
+
amount_sats: PRICES[service],
|
|
45
|
+
idempotency_key: idempotencyKey,
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
throw new Error(`Payment creation failed: ${response.statusText}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return await response.json();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check payment status
|
|
58
|
+
*/
|
|
59
|
+
async function checkPayment(invoiceId) {
|
|
60
|
+
const response = await fetch(
|
|
61
|
+
`${UGARAPI_BASE_URL}/api/v1/payment/${invoiceId}`
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
throw new Error(`Payment check failed: ${response.statusText}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return await response.json();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Wait for payment to be confirmed (with timeout)
|
|
73
|
+
*/
|
|
74
|
+
async function waitForPayment(invoiceId, timeoutMs = 300000) {
|
|
75
|
+
const startTime = Date.now();
|
|
76
|
+
|
|
77
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
78
|
+
const status = await checkPayment(invoiceId);
|
|
79
|
+
|
|
80
|
+
if (status.status === "paid") {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (status.status === "expired") {
|
|
85
|
+
throw new Error("Invoice expired before payment");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Wait 2 seconds before checking again
|
|
89
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
throw new Error("Payment timeout - invoice not paid in time");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Call UgarAPI service with payment
|
|
97
|
+
*/
|
|
98
|
+
async function callService(service, endpoint, payload, idempotencyKey) {
|
|
99
|
+
// Check if we have a cached payment for this idempotency key
|
|
100
|
+
let invoiceId = paymentCache.get(idempotencyKey);
|
|
101
|
+
|
|
102
|
+
if (!invoiceId) {
|
|
103
|
+
// Create new invoice
|
|
104
|
+
const invoice = await createInvoice(service, idempotencyKey);
|
|
105
|
+
invoiceId = invoice.invoice_id;
|
|
106
|
+
paymentCache.set(idempotencyKey, invoiceId);
|
|
107
|
+
|
|
108
|
+
// In a real implementation, you'd prompt the user to pay here
|
|
109
|
+
// For now, we'll simulate immediate payment (in production this would wait)
|
|
110
|
+
console.error(
|
|
111
|
+
`Payment required: ${invoice.payment_request} (${invoice.amount_sats} sats)`
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Simulate payment webhook (REMOVE THIS IN PRODUCTION)
|
|
115
|
+
await fetch(`${UGARAPI_BASE_URL}/api/v1/payment/webhook`, {
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: { "Content-Type": "application/json" },
|
|
118
|
+
body: JSON.stringify({
|
|
119
|
+
invoiceId: invoiceId,
|
|
120
|
+
status: "paid",
|
|
121
|
+
}),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Wait for payment confirmation
|
|
126
|
+
await waitForPayment(invoiceId);
|
|
127
|
+
|
|
128
|
+
// Call the actual service
|
|
129
|
+
const response = await fetch(`${UGARAPI_BASE_URL}${endpoint}`, {
|
|
130
|
+
method: "POST",
|
|
131
|
+
headers: { "Content-Type": "application/json" },
|
|
132
|
+
body: JSON.stringify({
|
|
133
|
+
...payload,
|
|
134
|
+
payment_proof: invoiceId,
|
|
135
|
+
idempotency_key: idempotencyKey,
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
const error = await response.json();
|
|
141
|
+
throw new Error(`Service call failed: ${JSON.stringify(error)}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return await response.json();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Create MCP server
|
|
148
|
+
const server = new Server(
|
|
149
|
+
{
|
|
150
|
+
name: "ugarapi",
|
|
151
|
+
version: "1.1.0",
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
capabilities: {
|
|
155
|
+
tools: {},
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// List available tools
|
|
161
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
162
|
+
return {
|
|
163
|
+
tools: [
|
|
164
|
+
{
|
|
165
|
+
name: "extract_web_data",
|
|
166
|
+
description:
|
|
167
|
+
"Extract structured data from any URL using CSS selectors. Returns the extracted content from the webpage. Costs 1000 sats (~$1) per extraction.",
|
|
168
|
+
inputSchema: {
|
|
169
|
+
type: "object",
|
|
170
|
+
properties: {
|
|
171
|
+
url: {
|
|
172
|
+
type: "string",
|
|
173
|
+
description: "The URL to extract data from",
|
|
174
|
+
},
|
|
175
|
+
selectors: {
|
|
176
|
+
type: "object",
|
|
177
|
+
description:
|
|
178
|
+
"CSS selectors to extract specific elements. Format: {'key': 'selector'}. Example: {'title': 'h1', 'price': '.product-price'}",
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
required: ["url", "selectors"],
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: "timestamp_document",
|
|
186
|
+
description:
|
|
187
|
+
"Create a cryptographic timestamp proof for a document on the Bitcoin blockchain. Returns verification URL and merkle root. Costs 5000 sats (~$5) per document.",
|
|
188
|
+
inputSchema: {
|
|
189
|
+
type: "object",
|
|
190
|
+
properties: {
|
|
191
|
+
document_hash: {
|
|
192
|
+
type: "string",
|
|
193
|
+
description:
|
|
194
|
+
"SHA-256 hash of the document to timestamp (64 character hex string)",
|
|
195
|
+
},
|
|
196
|
+
metadata: {
|
|
197
|
+
type: "object",
|
|
198
|
+
description:
|
|
199
|
+
"Optional metadata to include in the timestamp (e.g., author, title, description)",
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
required: ["document_hash"],
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: "aggregate_api_call",
|
|
207
|
+
description:
|
|
208
|
+
"Make API calls to external services (weather, maps, exchange rates) through a unified interface with automatic failover. Costs 200 sats (~$0.20) per call.",
|
|
209
|
+
inputSchema: {
|
|
210
|
+
type: "object",
|
|
211
|
+
properties: {
|
|
212
|
+
service: {
|
|
213
|
+
type: "string",
|
|
214
|
+
enum: ["weather", "maps", "exchange_rate"],
|
|
215
|
+
description: "Which API service to call",
|
|
216
|
+
},
|
|
217
|
+
endpoint: {
|
|
218
|
+
type: "string",
|
|
219
|
+
description:
|
|
220
|
+
"The endpoint path (e.g., for weather: city name, for exchange_rate: currency code)",
|
|
221
|
+
},
|
|
222
|
+
params: {
|
|
223
|
+
type: "object",
|
|
224
|
+
description: "Query parameters for the API call",
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
required: ["service", "endpoint", "params"],
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
};
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Handle tool calls
|
|
235
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
236
|
+
const { name, arguments: args } = request.params;
|
|
237
|
+
|
|
238
|
+
// Generate idempotency key from tool call
|
|
239
|
+
const idempotencyKey = `mcp_${name}_${Date.now()}_${Math.random()
|
|
240
|
+
.toString(36)
|
|
241
|
+
.substr(2, 9)}`;
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
switch (name) {
|
|
245
|
+
case "extract_web_data": {
|
|
246
|
+
const result = await callService(
|
|
247
|
+
"web_extraction",
|
|
248
|
+
"/api/v1/extract",
|
|
249
|
+
{
|
|
250
|
+
url: args.url,
|
|
251
|
+
selectors: args.selectors,
|
|
252
|
+
},
|
|
253
|
+
idempotencyKey
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
content: [
|
|
258
|
+
{
|
|
259
|
+
type: "text",
|
|
260
|
+
text: JSON.stringify(result, null, 2),
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
case "timestamp_document": {
|
|
267
|
+
const result = await callService(
|
|
268
|
+
"document_timestamp",
|
|
269
|
+
"/api/v1/timestamp",
|
|
270
|
+
{
|
|
271
|
+
document_hash: args.document_hash,
|
|
272
|
+
metadata: args.metadata || {},
|
|
273
|
+
},
|
|
274
|
+
idempotencyKey
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
content: [
|
|
279
|
+
{
|
|
280
|
+
type: "text",
|
|
281
|
+
text: JSON.stringify(result, null, 2),
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
case "aggregate_api_call": {
|
|
288
|
+
const result = await callService(
|
|
289
|
+
"api_aggregator",
|
|
290
|
+
"/api/v1/aggregate",
|
|
291
|
+
{
|
|
292
|
+
service: args.service,
|
|
293
|
+
endpoint: args.endpoint,
|
|
294
|
+
params: args.params,
|
|
295
|
+
},
|
|
296
|
+
idempotencyKey
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
content: [
|
|
301
|
+
{
|
|
302
|
+
type: "text",
|
|
303
|
+
text: JSON.stringify(result, null, 2),
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
default:
|
|
310
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
311
|
+
}
|
|
312
|
+
} catch (error) {
|
|
313
|
+
return {
|
|
314
|
+
content: [
|
|
315
|
+
{
|
|
316
|
+
type: "text",
|
|
317
|
+
text: `Error: ${error.message}`,
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
isError: true,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Start server
|
|
326
|
+
async function main() {
|
|
327
|
+
const transport = new StdioServerTransport();
|
|
328
|
+
await server.connect(transport);
|
|
329
|
+
console.error("UgarAPI MCP Server running on stdio");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
main().catch((error) => {
|
|
333
|
+
console.error("Fatal error:", error);
|
|
334
|
+
process.exit(1);
|
|
335
|
+
});
|