@snokam/mcp-api 0.8.0 → 0.10.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/dist/index.js CHANGED
@@ -9,7 +9,7 @@
9
9
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
10
10
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
11
11
  import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
12
- import { fetchSpecs } from "./openapi-loader.js";
12
+ import { fetchSpecs, fetchSpecFromUrl, } from "./openapi-loader.js";
13
13
  import { getAccessToken } from "./auth.js";
14
14
  // ---------------------------------------------------------------------------
15
15
  // State
@@ -161,12 +161,9 @@ async function executeCall(endpoint, args) {
161
161
  const headers = {
162
162
  Accept: "application/json",
163
163
  };
164
- const isLocal = url.startsWith("http://localhost") || url.startsWith("http://127.0.0.1");
165
- if (!isLocal) {
166
- const token = await getAccessToken(endpoint.scope);
167
- if (token) {
168
- headers.Authorization = `Bearer ${token}`;
169
- }
164
+ const token = await getAccessToken(endpoint.scope);
165
+ if (token) {
166
+ headers.Authorization = `Bearer ${token}`;
170
167
  }
171
168
  let fetchBody;
172
169
  if (args.body !== undefined && endpoint.method !== "GET") {
@@ -348,6 +345,39 @@ Controls: Sonos speakers, lights, YouTube queue
348
345
  if (name === SET_URL_TOOL_NAME) {
349
346
  const service = String(args.service ?? "");
350
347
  const url = String(args.url ?? "");
348
+ const isLocal = url.startsWith("http://localhost") ||
349
+ url.startsWith("http://127.0.0.1");
350
+ // For localhost URLs, fetch the live swagger spec from the local function
351
+ // to pick up any new/changed endpoints during development
352
+ if (isLocal) {
353
+ try {
354
+ const liveEndpoints = await fetchSpecFromUrl(service, url);
355
+ // Remove old endpoints for this service
356
+ endpoints = endpoints.filter((ep) => ep.service !== service);
357
+ // Add the fresh ones
358
+ endpoints.push(...liveEndpoints);
359
+ // Rebuild lookup map
360
+ endpointsByTool = new Map();
361
+ for (const ep of endpoints) {
362
+ endpointsByTool.set(ep.toolName, ep);
363
+ }
364
+ serviceUrlOverrides.set(service, url);
365
+ await server.sendToolListChanged();
366
+ return {
367
+ content: [
368
+ {
369
+ type: "text",
370
+ text: `Overrode ${service} → ${url}. Loaded ${liveEndpoints.length} endpoints from local swagger.`,
371
+ },
372
+ ],
373
+ };
374
+ }
375
+ catch (error) {
376
+ // Fall through to static override if swagger fetch fails
377
+ console.error(`[snokam-mcp] Failed to fetch swagger from ${url}, falling back to static spec:`, error instanceof Error ? error.message : error);
378
+ }
379
+ }
380
+ // Static override: just change the base URL on existing endpoints
351
381
  const serviceEndpoints = endpoints.filter((ep) => ep.service === service);
352
382
  if (serviceEndpoints.length === 0) {
353
383
  const available = [
@@ -371,7 +401,7 @@ Controls: Sonos speakers, lights, YouTube queue
371
401
  content: [
372
402
  {
373
403
  type: "text",
374
- text: `Overrode ${service} → ${url} (${serviceEndpoints.length} endpoints). Auth ${url.startsWith("http://localhost") || url.startsWith("http://127.0.0.1") ? "skipped" : "active"}.`,
404
+ text: `Overrode ${service} → ${url} (${serviceEndpoints.length} endpoints).`,
375
405
  },
376
406
  ],
377
407
  };
@@ -42,5 +42,10 @@ interface OpenApiRequestBody {
42
42
  schema?: Record<string, unknown>;
43
43
  }>;
44
44
  }
45
+ /**
46
+ * Fetch a swagger spec from a specific URL (e.g. a locally running function)
47
+ * and return parsed endpoints for that service.
48
+ */
49
+ export declare function fetchSpecFromUrl(service: string, baseUrl: string): Promise<ApiEndpoint[]>;
45
50
  export declare function fetchSpecs(environment: string): Promise<ApiEndpoint[]>;
46
51
  export {};
@@ -101,6 +101,21 @@ function parseSpec(spec, service, baseUrl) {
101
101
  // ---------------------------------------------------------------------------
102
102
  // Public API
103
103
  // ---------------------------------------------------------------------------
104
+ /**
105
+ * Fetch a swagger spec from a specific URL (e.g. a locally running function)
106
+ * and return parsed endpoints for that service.
107
+ */
108
+ export async function fetchSpecFromUrl(service, baseUrl) {
109
+ const swaggerUrl = `${baseUrl}/swagger.json`;
110
+ const response = await fetch(swaggerUrl, {
111
+ signal: AbortSignal.timeout(10_000),
112
+ });
113
+ if (!response.ok) {
114
+ throw new Error(`HTTP ${response.status} from ${swaggerUrl}`);
115
+ }
116
+ const spec = (await response.json());
117
+ return parseSpec(spec, service, baseUrl);
118
+ }
104
119
  export async function fetchSpecs(environment) {
105
120
  // Try to load bundled specs first (for speed)
106
121
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snokam/mcp-api",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "MCP server exposing Snokam backend APIs as tools for Claude Code and other MCP clients",
5
5
  "type": "module",
6
6
  "bin": {
@@ -339,6 +339,9 @@
339
339
  "body": {
340
340
  "type": "string"
341
341
  },
342
+ "email": {
343
+ "type": "string"
344
+ },
342
345
  "tags": {
343
346
  "type": "array",
344
347
  "items": {
@@ -339,6 +339,9 @@
339
339
  "body": {
340
340
  "type": "string"
341
341
  },
342
+ "email": {
343
+ "type": "string"
344
+ },
342
345
  "tags": {
343
346
  "type": "array",
344
347
  "items": {
@@ -0,0 +1,181 @@
1
+ {
2
+ "openapi": "3.0.1",
3
+ "info": {
4
+ "title": "Sync Function",
5
+ "description": "Sync data from one system to another, e.g CvPartner to Sanity",
6
+ "version": "v1.0.0"
7
+ },
8
+ "servers": [
9
+ {
10
+ "url": "https://sync.api.test.snokam.no"
11
+ }
12
+ ],
13
+ "paths": {
14
+ "/v1.0/azure-ad-to-sanity": {
15
+ "get": {
16
+ "tags": [
17
+ "Sync"
18
+ ],
19
+ "summary": "Syncs Azure AD to Sanity",
20
+ "description": "Synchronizes data from Azure AD to Sanity.",
21
+ "operationId": "SyncAzureAdToSanity",
22
+ "responses": {
23
+ "200": {
24
+ "description": "Data synchronized successfully",
25
+ "content": {
26
+ "application/json": {
27
+ "schema": {
28
+ "type": "string"
29
+ }
30
+ }
31
+ },
32
+ "x-ms-summary": "Success"
33
+ },
34
+ "401": {
35
+ "description": "Unauthorized access",
36
+ "x-ms-summary": "Unauthorized"
37
+ }
38
+ },
39
+ "security": [
40
+ {
41
+ "Implicit": [
42
+ "api://2d964377-8322-482d-bd45-3134b48a9da5/user_impersonation"
43
+ ]
44
+ }
45
+ ]
46
+ }
47
+ },
48
+ "/v1.0/cv-partner-to-sanity": {
49
+ "get": {
50
+ "tags": [
51
+ "Sync"
52
+ ],
53
+ "summary": "Syncs CV Partner to Sanity",
54
+ "description": "Synchronizes data from CV Partner to Sanity.",
55
+ "operationId": "SyncCvPartnerToSanity",
56
+ "responses": {
57
+ "200": {
58
+ "description": "Data synchronized successfully",
59
+ "content": {
60
+ "application/json": {
61
+ "schema": {
62
+ "type": "string"
63
+ }
64
+ }
65
+ },
66
+ "x-ms-summary": "Success"
67
+ },
68
+ "401": {
69
+ "description": "Unauthorized access",
70
+ "x-ms-summary": "Unauthorized"
71
+ }
72
+ },
73
+ "security": [
74
+ {
75
+ "Implicit": [
76
+ "api://2d964377-8322-482d-bd45-3134b48a9da5/user_impersonation"
77
+ ]
78
+ }
79
+ ]
80
+ }
81
+ },
82
+ "/v1.0/power-office-to-sanity": {
83
+ "get": {
84
+ "tags": [
85
+ "Sync"
86
+ ],
87
+ "summary": "Sync power office data to sanity",
88
+ "description": "Sync power office data to sanity",
89
+ "operationId": "SyncPowerOfficeToSanity",
90
+ "parameters": [
91
+ {
92
+ "name": "from",
93
+ "in": "query",
94
+ "description": "",
95
+ "required": true,
96
+ "schema": {
97
+ "type": "string",
98
+ "format": "date-time"
99
+ },
100
+ "x-ms-summary": "Date from"
101
+ },
102
+ {
103
+ "name": "to",
104
+ "in": "query",
105
+ "description": "",
106
+ "required": true,
107
+ "schema": {
108
+ "type": "string",
109
+ "format": "date-time"
110
+ },
111
+ "x-ms-summary": "Date to"
112
+ }
113
+ ],
114
+ "responses": {
115
+ "200": {
116
+ "description": "Data synchronized successfully",
117
+ "content": {
118
+ "application/json": {
119
+ "schema": {
120
+ "type": "string"
121
+ }
122
+ }
123
+ },
124
+ "x-ms-summary": "Success"
125
+ },
126
+ "401": {
127
+ "description": "Unauthorized access",
128
+ "x-ms-summary": "Unauthorized"
129
+ }
130
+ },
131
+ "security": [
132
+ {
133
+ "Implicit": [
134
+ "api://2d964377-8322-482d-bd45-3134b48a9da5/user_impersonation"
135
+ ]
136
+ }
137
+ ]
138
+ }
139
+ },
140
+ "/v1.0/okrs-to-sanity": {
141
+ "get": {
142
+ "tags": [
143
+ "Sync"
144
+ ],
145
+ "summary": "Sync OKRs",
146
+ "description": "Synchronizes OKRs",
147
+ "operationId": "SyncOkrs",
148
+ "responses": {
149
+ "200": {
150
+ "description": "OKRs synchronized successfully",
151
+ "x-ms-summary": "Success"
152
+ }
153
+ },
154
+ "security": [
155
+ {
156
+ "Implicit": [
157
+ "api://2d964377-8322-482d-bd45-3134b48a9da5/user_impersonation"
158
+ ]
159
+ }
160
+ ]
161
+ }
162
+ }
163
+ },
164
+ "components": {
165
+ "securitySchemes": {
166
+ "Implicit": {
167
+ "type": "oauth2",
168
+ "flows": {
169
+ "implicit": {
170
+ "authorizationUrl": "https://login.microsoftonline.com/a8533784-aa3c-403b-a61a-1533ecc6e3ed/oauth2/v2.0/authorize",
171
+ "tokenUrl": "https://login.microsoftonline.com/a8533784-aa3c-403b-a61a-1533ecc6e3ed/oauth2/v2.0/token",
172
+ "refreshUrl": "https://login.microsoftonline.com/a8533784-aa3c-403b-a61a-1533ecc6e3ed/oauth2/v2.0/token",
173
+ "scopes": {
174
+ "api://2d964377-8322-482d-bd45-3134b48a9da5/user_impersonation": "Default function scope"
175
+ }
176
+ }
177
+ }
178
+ }
179
+ }
180
+ }
181
+ }