@centia-io/mcp-server 1.0.5 → 1.0.7

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.
Files changed (4) hide show
  1. package/README.md +148 -131
  2. package/centia-api.json +3946 -3134
  3. package/dist/index.js +112 -15
  4. package/package.json +33 -32
package/dist/index.js CHANGED
@@ -6,11 +6,39 @@ import axios from "axios";
6
6
  import fs from "fs";
7
7
  import path from "path";
8
8
  import { fileURLToPath } from "url";
9
+ import { createConfigstoreTokenStore, createTokenProvider, CodeFlow, NotLoggedInError, SessionExpiredError, } from "@centia-io/sdk";
9
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
11
  const apiSpecPath = path.join(__dirname, "..", "centia-api.json");
11
12
  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;
13
+ const DEFAULT_HOST = "https://api.centia.io";
14
+ const tokenStore = createConfigstoreTokenStore("gc2-env"); // shared with gc2-cli
15
+ async function getApiBaseUrl() {
16
+ if (process.env.API_BASE_URL)
17
+ return process.env.API_BASE_URL;
18
+ const stored = await tokenStore.get();
19
+ return stored.host || DEFAULT_HOST;
20
+ }
21
+ let cachedProvider = null;
22
+ async function getTokenProvider() {
23
+ if (!cachedProvider) {
24
+ const host = await getApiBaseUrl();
25
+ // TODO: register a separate "gc2-mcp" OAuth client server-side and switch
26
+ const authService = new CodeFlow({
27
+ host,
28
+ clientId: "gc2-cli",
29
+ redirectUri: "http://127.0.0.1:5657/auth/callback",
30
+ }).service;
31
+ cachedProvider = createTokenProvider({ store: tokenStore, authService });
32
+ }
33
+ return cachedProvider;
34
+ }
35
+ async function getAccessToken() {
36
+ // Env wins for CI / headless
37
+ if (process.env.API_TOKEN)
38
+ return process.env.API_TOKEN;
39
+ const provider = await getTokenProvider();
40
+ return provider.getAccessToken();
41
+ }
14
42
  const server = new Server({
15
43
  name: "centia-io-mcp-server",
16
44
  version: "1.0.0",
@@ -341,14 +369,36 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
341
369
  if (!toolMeta) {
342
370
  throw new Error(`Tool not found: ${name}`);
343
371
  }
344
- let url = `${API_BASE_URL}${toolMeta.path}`;
372
+ let url = `${await getApiBaseUrl()}${toolMeta.path}`;
345
373
  const config = {
346
374
  method: toolMeta.method,
347
375
  headers: {},
348
376
  params: {},
349
377
  };
350
- if (API_TOKEN) {
351
- config.headers["Authorization"] = `Bearer ${API_TOKEN}`;
378
+ try {
379
+ config.headers["Authorization"] = `Bearer ${await getAccessToken()}`;
380
+ }
381
+ catch (e) {
382
+ if (e instanceof NotLoggedInError) {
383
+ return {
384
+ isError: true,
385
+ content: [{
386
+ type: "text",
387
+ text: "Not logged in to Centia. Run `gc2 login` (npm i -g @mapcentia/gc2-cli) " +
388
+ "or set API_TOKEN env var.",
389
+ }],
390
+ };
391
+ }
392
+ if (e instanceof SessionExpiredError) {
393
+ return {
394
+ isError: true,
395
+ content: [{
396
+ type: "text",
397
+ text: "Centia session expired. Run `gc2 login` again.",
398
+ }],
399
+ };
400
+ }
401
+ throw e;
352
402
  }
353
403
  const safeArgs = args || {};
354
404
  // Path parameters
@@ -410,30 +460,77 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
410
460
  config.data = safeArgs.requestBody;
411
461
  }
412
462
  }
463
+ const sendRequest = () => axios({
464
+ ...config,
465
+ url,
466
+ maxRedirects: 0,
467
+ validateStatus: (status) => status < 400,
468
+ });
469
+ const formatSuccess = (response) => ({
470
+ content: [
471
+ {
472
+ type: "text",
473
+ text: response.data != null ? JSON.stringify(response.data, null, 2) : response.statusText,
474
+ },
475
+ ],
476
+ });
413
477
  try {
414
- const response = await axios({ ...config, url });
415
- return {
416
- content: [
417
- {
418
- type: "text",
419
- text: response.status === 200 ? JSON.stringify(response.data, null, 2) : response.statusText,
420
- },
421
- ],
422
- };
478
+ return formatSuccess(await sendRequest());
423
479
  }
424
480
  catch (error) {
481
+ // 401: force a token refresh and retry once. Skip when an explicit
482
+ // API_TOKEN env is in use — refresh isn't possible there.
483
+ if (error.response?.status === 401 && !process.env.API_TOKEN) {
484
+ try {
485
+ await tokenStore.set({ token: undefined });
486
+ const refreshed = await getAccessToken();
487
+ config.headers["Authorization"] = `Bearer ${refreshed}`;
488
+ return formatSuccess(await sendRequest());
489
+ }
490
+ catch (retryError) {
491
+ if (retryError instanceof NotLoggedInError || retryError instanceof SessionExpiredError) {
492
+ return {
493
+ isError: true,
494
+ content: [{
495
+ type: "text",
496
+ text: "Centia session expired. Run `gc2 login` again.",
497
+ }],
498
+ };
499
+ }
500
+ error = retryError;
501
+ }
502
+ }
425
503
  return {
426
504
  isError: true,
427
505
  content: [
428
506
  {
429
507
  type: "text",
430
- text: error.response.data.message || error.message,
508
+ text: error.response?.data?.message || error.message,
431
509
  },
432
510
  ],
433
511
  };
434
512
  }
435
513
  });
514
+ async function logAuthStatus() {
515
+ if (process.env.API_TOKEN) {
516
+ console.error("Centia MCP: using API_TOKEN env");
517
+ return;
518
+ }
519
+ const stored = await tokenStore.get();
520
+ if (!stored.token) {
521
+ console.error("Centia MCP: no login found. Run `gc2 login` or set API_TOKEN.");
522
+ return;
523
+ }
524
+ try {
525
+ const claims = JSON.parse(Buffer.from(stored.token.split(".")[1], "base64").toString("utf-8"));
526
+ console.error(`Centia MCP: logged in as ${claims.uid} on ${stored.host || DEFAULT_HOST}`);
527
+ }
528
+ catch {
529
+ console.error("Centia MCP: token present but unreadable");
530
+ }
531
+ }
436
532
  async function main() {
533
+ await logAuthStatus();
437
534
  const transport = new StdioServerTransport();
438
535
  await server.connect(transport);
439
536
  console.error("Centia MCP Server running on stdio");
package/package.json CHANGED
@@ -1,32 +1,33 @@
1
- {
2
- "name": "@centia-io/mcp-server",
3
- "version": "1.0.5",
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
- }
1
+ {
2
+ "name": "@centia-io/mcp-server",
3
+ "version": "1.0.7",
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
+ "@centia-io/sdk": "^0.1.0",
24
+ "@modelcontextprotocol/sdk": "^1.26.0",
25
+ "axios": "^1.13.5",
26
+ "zod": "^4.3.6"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^25.2.2",
30
+ "tsx": "^4.21.0",
31
+ "typescript": "^5.9.3"
32
+ }
33
+ }