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.
@@ -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
+ });