@centia-io/mcp-server 1.0.1

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/dist/index.js ADDED
@@ -0,0 +1,410 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
+ import axios from "axios";
6
+ import fs from "fs";
7
+ import path from "path";
8
+ import { fileURLToPath } from "url";
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ const apiSpecPath = path.join(__dirname, "..", "centia-api.json");
11
+ const apiSpec = JSON.parse(fs.readFileSync(apiSpecPath, "utf-8"));
12
+ const API_BASE_URL = process.env.API_BASE_URL || "https://api.centia.io";
13
+ const API_TOKEN = process.env.API_TOKEN;
14
+ const server = new Server({
15
+ name: "centia-io-mcp-server",
16
+ version: "1.0.0",
17
+ }, {
18
+ capabilities: {
19
+ tools: {},
20
+ },
21
+ });
22
+ function resolveSchema(schema) {
23
+ if (!schema)
24
+ return { type: "string" };
25
+ if (schema.$ref) {
26
+ const refPath = schema.$ref.replace("#/", "").split("/");
27
+ let current = apiSpec;
28
+ for (const segment of refPath) {
29
+ if (!current[segment]) {
30
+ console.error(`Could not resolve ref: ${schema.$ref}`);
31
+ return { type: "string" };
32
+ }
33
+ current = current[segment];
34
+ }
35
+ return resolveSchema(current);
36
+ }
37
+ if (schema.allOf) {
38
+ let merged = { type: "object", properties: {}, required: [] };
39
+ for (const s of schema.allOf) {
40
+ const resolved = resolveSchema(s);
41
+ if (resolved.properties) {
42
+ merged.properties = { ...merged.properties, ...resolved.properties };
43
+ }
44
+ if (resolved.required) {
45
+ merged.required = [...new Set([...merged.required, ...resolved.required])];
46
+ }
47
+ }
48
+ return merged;
49
+ }
50
+ if (schema.type === "object" && schema.properties) {
51
+ const properties = {};
52
+ for (const [key, value] of Object.entries(schema.properties)) {
53
+ properties[key] = resolveSchema(value);
54
+ }
55
+ return { ...schema, properties };
56
+ }
57
+ if (schema.type === "array" && schema.items) {
58
+ return { ...schema, items: resolveSchema(schema.items) };
59
+ }
60
+ return schema;
61
+ }
62
+ // Sanitize JSON Schemas for MCP tool input to avoid oversized numeric literals
63
+ function sanitizeSchemaForMCP(schema) {
64
+ if (schema == null)
65
+ return schema;
66
+ if (Array.isArray(schema)) {
67
+ return schema.map((s) => sanitizeSchemaForMCP(s));
68
+ }
69
+ if (typeof schema !== "object")
70
+ return schema;
71
+ const clone = { ...schema };
72
+ const numericKeys = [
73
+ "example",
74
+ "default",
75
+ "maximum",
76
+ "minimum",
77
+ "exclusiveMaximum",
78
+ "exclusiveMinimum",
79
+ "multipleOf",
80
+ "const",
81
+ ];
82
+ for (const key of numericKeys) {
83
+ if (clone[key] !== undefined && typeof clone[key] === "number") {
84
+ if (!Number.isFinite(clone[key]) || Math.abs(clone[key]) > Number.MAX_SAFE_INTEGER) {
85
+ delete clone[key];
86
+ }
87
+ }
88
+ }
89
+ // Drop non‑standard/annotation keywords that some validators reject
90
+ for (const k of ["example", "examples", "deprecated", "readOnly", "writeOnly", "xml", "style", "explode", "nullable"]) {
91
+ if (k in clone)
92
+ delete clone[k];
93
+ }
94
+ if (Array.isArray(clone.enum)) {
95
+ clone.enum = clone.enum.filter((v) => !(typeof v === "number" && (!Number.isFinite(v) || Math.abs(v) > Number.MAX_SAFE_INTEGER)));
96
+ if (clone.enum.length === 0)
97
+ delete clone.enum;
98
+ }
99
+ if (clone.properties && typeof clone.properties === "object") {
100
+ const newProps = {};
101
+ for (const [k, v] of Object.entries(clone.properties)) {
102
+ newProps[k] = sanitizeSchemaForMCP(v);
103
+ }
104
+ clone.properties = newProps;
105
+ }
106
+ if (clone.items) {
107
+ clone.items = sanitizeSchemaForMCP(clone.items);
108
+ }
109
+ for (const k of ["allOf", "anyOf", "oneOf"]) {
110
+ if (Array.isArray(clone[k])) {
111
+ clone[k] = clone[k].map((s) => sanitizeSchemaForMCP(s));
112
+ }
113
+ }
114
+ return clone;
115
+ }
116
+ // Normalize a schema to a conservative JSON Schema 2020-12 subset accepted by most MCP clients
117
+ function normalizeJsonSchemaForMCP(schema) {
118
+ if (schema == null)
119
+ return undefined;
120
+ if (Array.isArray(schema))
121
+ return schema.map((s) => normalizeJsonSchemaForMCP(s));
122
+ if (typeof schema !== "object")
123
+ return schema;
124
+ const allowedKeys = new Set([
125
+ "type",
126
+ "properties",
127
+ "required",
128
+ "description",
129
+ "enum",
130
+ "items",
131
+ "anyOf",
132
+ "oneOf",
133
+ "allOf",
134
+ "const",
135
+ "default",
136
+ "minimum",
137
+ "maximum",
138
+ "exclusiveMinimum",
139
+ "exclusiveMaximum",
140
+ "multipleOf",
141
+ "minLength",
142
+ "maxLength",
143
+ "pattern",
144
+ "format",
145
+ "minItems",
146
+ "maxItems",
147
+ "uniqueItems",
148
+ "contains",
149
+ "additionalProperties",
150
+ "patternProperties",
151
+ "title",
152
+ ]);
153
+ const formatWhitelist = new Set([
154
+ "email",
155
+ "uri",
156
+ "uuid",
157
+ "ipv4",
158
+ "ipv6",
159
+ "date-time",
160
+ "date",
161
+ "time",
162
+ ]);
163
+ const result = {};
164
+ for (const [k, v] of Object.entries(schema)) {
165
+ if (!allowedKeys.has(k))
166
+ continue; // drop unknown keys
167
+ result[k] = v;
168
+ }
169
+ // Ensure type validity or provide a safe default
170
+ const validTypes = new Set(["string", "number", "integer", "boolean", "object", "array", "null"]);
171
+ if (result.type !== undefined) {
172
+ if (Array.isArray(result.type)) {
173
+ const filtered = result.type.filter((t) => validTypes.has(t));
174
+ if (filtered.length > 0)
175
+ result.type = filtered;
176
+ else
177
+ delete result.type;
178
+ }
179
+ else if (!validTypes.has(result.type)) {
180
+ delete result.type;
181
+ }
182
+ }
183
+ // Prune/normalize format
184
+ if (typeof result.format === "string" && !formatWhitelist.has(result.format)) {
185
+ delete result.format; // remove non-standard formats like 'url' or 'binary'
186
+ }
187
+ // Normalize required
188
+ if (result.required) {
189
+ if (!Array.isArray(result.required))
190
+ delete result.required;
191
+ else {
192
+ const onlyStrings = result.required.filter((r) => typeof r === "string");
193
+ if (onlyStrings.length > 0)
194
+ result.required = Array.from(new Set(onlyStrings));
195
+ else
196
+ delete result.required;
197
+ }
198
+ }
199
+ // Recurse
200
+ if (result.properties && typeof result.properties === "object") {
201
+ const newProps = {};
202
+ for (const [k, v] of Object.entries(result.properties)) {
203
+ const norm = normalizeJsonSchemaForMCP(sanitizeSchemaForMCP(v));
204
+ if (norm)
205
+ newProps[k] = norm;
206
+ }
207
+ result.properties = newProps;
208
+ if (Object.keys(result.properties).length === 0)
209
+ delete result.properties;
210
+ }
211
+ if (result.items) {
212
+ result.items = normalizeJsonSchemaForMCP(sanitizeSchemaForMCP(result.items));
213
+ if (result.items === undefined)
214
+ delete result.items;
215
+ }
216
+ for (const key of ["anyOf", "oneOf", "allOf"]) {
217
+ if (Array.isArray(result[key])) {
218
+ result[key] = result[key]
219
+ .map((s) => normalizeJsonSchemaForMCP(sanitizeSchemaForMCP(s)))
220
+ .filter(Boolean);
221
+ if (result[key].length === 0)
222
+ delete result[key];
223
+ }
224
+ }
225
+ if (result.enum && Array.isArray(result.enum)) {
226
+ // Ensure enums are unique and non-empty
227
+ const uniq = Array.from(new Set(result.enum));
228
+ if (uniq.length > 0)
229
+ result.enum = uniq;
230
+ else
231
+ delete result.enum;
232
+ }
233
+ // If nothing constrains the schema, default to a string to keep it simple
234
+ const hasConstraints = result.type !== undefined ||
235
+ result.enum !== undefined ||
236
+ result.const !== undefined ||
237
+ result.anyOf !== undefined ||
238
+ result.oneOf !== undefined ||
239
+ result.allOf !== undefined ||
240
+ result.properties !== undefined ||
241
+ result.items !== undefined;
242
+ if (!hasConstraints) {
243
+ result.type = "string";
244
+ }
245
+ return result;
246
+ }
247
+ const tools = [];
248
+ const operationMap = new Map();
249
+ for (const [pathStr, pathItem] of Object.entries(apiSpec.paths)) {
250
+ for (const [method, operation] of Object.entries(pathItem)) {
251
+ if (method === "parameters")
252
+ continue;
253
+ const op = operation;
254
+ const operationId = op.operationId || `${method}${pathStr.replace(/[\/\W]/g, '_')}`;
255
+ const parameters = [...(pathItem.parameters || []), ...(op.parameters || [])];
256
+ const requestBody = op.requestBody;
257
+ const properties = {};
258
+ const required = [];
259
+ const toolMeta = {
260
+ method,
261
+ path: pathStr,
262
+ pathParams: [],
263
+ queryParams: [],
264
+ headerParams: [],
265
+ bodyParams: [],
266
+ isBodyFlattened: false,
267
+ };
268
+ // Handle path/query/header parameters
269
+ for (const param of parameters) {
270
+ const resolvedParam = param.$ref ? resolveSchema(param) : param;
271
+ const paramSchema = resolveSchema(resolvedParam.schema);
272
+ const sanitizedParamSchema = sanitizeSchemaForMCP(paramSchema);
273
+ const normalizedParamSchema = normalizeJsonSchemaForMCP(sanitizedParamSchema);
274
+ properties[resolvedParam.name] = {
275
+ ...normalizedParamSchema,
276
+ description: resolvedParam.description || normalizedParamSchema?.description,
277
+ };
278
+ if (resolvedParam.required) {
279
+ required.push(resolvedParam.name);
280
+ }
281
+ if (resolvedParam.in === "path")
282
+ toolMeta.pathParams.push(resolvedParam.name);
283
+ if (resolvedParam.in === "query")
284
+ toolMeta.queryParams.push(resolvedParam.name);
285
+ if (resolvedParam.in === "header")
286
+ toolMeta.headerParams.push(resolvedParam.name);
287
+ }
288
+ // Handle request body
289
+ if (requestBody?.content?.["application/json"]?.schema) {
290
+ const bodySchema = resolveSchema(requestBody.content["application/json"].schema);
291
+ if (bodySchema.type === "object" && bodySchema.properties) {
292
+ toolMeta.isBodyFlattened = true;
293
+ for (const [key, value] of Object.entries(bodySchema.properties)) {
294
+ properties[key] = normalizeJsonSchemaForMCP(sanitizeSchemaForMCP(value));
295
+ toolMeta.bodyParams.push(key);
296
+ }
297
+ if (bodySchema.required) {
298
+ required.push(...bodySchema.required);
299
+ }
300
+ }
301
+ else {
302
+ properties.requestBody = normalizeJsonSchemaForMCP(sanitizeSchemaForMCP(bodySchema));
303
+ toolMeta.bodyParams.push("requestBody");
304
+ if (requestBody.required) {
305
+ required.push("requestBody");
306
+ }
307
+ }
308
+ }
309
+ tools.push({
310
+ name: operationId,
311
+ description: op.description || op.summary || `Execute ${method.toUpperCase()} ${pathStr}`,
312
+ inputSchema: {
313
+ type: "object",
314
+ properties,
315
+ ...(required.length > 0 ? { required: [...new Set(required)] } : {}),
316
+ },
317
+ });
318
+ operationMap.set(operationId, toolMeta);
319
+ }
320
+ }
321
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
322
+ tools,
323
+ }));
324
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
325
+ const { name, arguments: args } = request.params;
326
+ const toolMeta = operationMap.get(name);
327
+ if (!toolMeta) {
328
+ throw new Error(`Tool not found: ${name}`);
329
+ }
330
+ let url = `${API_BASE_URL}${toolMeta.path}`;
331
+ const config = {
332
+ method: toolMeta.method,
333
+ headers: {},
334
+ params: {},
335
+ };
336
+ if (API_TOKEN) {
337
+ config.headers["Authorization"] = `Bearer ${API_TOKEN}`;
338
+ }
339
+ const safeArgs = args || {};
340
+ // Path parameters
341
+ for (const paramName of toolMeta.pathParams) {
342
+ if (safeArgs[paramName] !== undefined) {
343
+ url = url.replace(`{${paramName}}`, encodeURIComponent(String(safeArgs[paramName])));
344
+ }
345
+ else {
346
+ url = url.replace(`/{${paramName}}`, "");
347
+ url = url.replace(`{${paramName}}`, "");
348
+ }
349
+ }
350
+ // Query parameters
351
+ for (const paramName of toolMeta.queryParams) {
352
+ if (safeArgs[paramName] !== undefined) {
353
+ config.params[paramName] = safeArgs[paramName];
354
+ }
355
+ }
356
+ // Header parameters
357
+ for (const paramName of toolMeta.headerParams) {
358
+ if (safeArgs[paramName] !== undefined) {
359
+ config.headers[paramName] = String(safeArgs[paramName]);
360
+ }
361
+ }
362
+ // Body
363
+ if (toolMeta.bodyParams.length > 0) {
364
+ if (toolMeta.isBodyFlattened) {
365
+ const body = {};
366
+ for (const paramName of toolMeta.bodyParams) {
367
+ if (safeArgs[paramName] !== undefined) {
368
+ body[paramName] = safeArgs[paramName];
369
+ }
370
+ }
371
+ config.data = body;
372
+ }
373
+ else {
374
+ config.data = safeArgs.requestBody;
375
+ }
376
+ }
377
+ try {
378
+ const response = await axios({ ...config, url });
379
+ return {
380
+ content: [
381
+ {
382
+ type: "text",
383
+ text: JSON.stringify(response.data, null, 2),
384
+ },
385
+ ],
386
+ };
387
+ }
388
+ catch (error) {
389
+ return {
390
+ isError: true,
391
+ content: [
392
+ {
393
+ type: "text",
394
+ text: error.response?.data
395
+ ? JSON.stringify(error.response.data, null, 2)
396
+ : error.message,
397
+ },
398
+ ],
399
+ };
400
+ }
401
+ });
402
+ async function main() {
403
+ const transport = new StdioServerTransport();
404
+ await server.connect(transport);
405
+ console.error("Centia MCP Server running on stdio");
406
+ }
407
+ main().catch((error) => {
408
+ console.error("Fatal error in main():", error);
409
+ process.exit(1);
410
+ });
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@centia-io/mcp-server",
3
+ "version": "1.0.1",
4
+ "publishConfig": { "access": "public" },
5
+ "description": "Centia MCP Server",
6
+ "type": "module",
7
+ "bin": {
8
+ "mcp-server": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "centia-api.json"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "start": "node dist/index.js",
17
+ "dev": "tsx src/index.ts"
18
+ },
19
+ "keywords": [],
20
+ "author": "",
21
+ "license": "ISC",
22
+ "dependencies": {
23
+ "@modelcontextprotocol/sdk": "^1.26.0",
24
+ "axios": "^1.13.5",
25
+ "zod": "^4.3.6"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^25.2.2",
29
+ "tsx": "^4.21.0",
30
+ "typescript": "^5.9.3"
31
+ }
32
+ }