@snokam/mcp-api 1.4.0 → 1.4.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.
Files changed (41) hide show
  1. package/dist/auth.d.ts +0 -15
  2. package/dist/auth.js +15 -59
  3. package/dist/builtin-tools.d.ts +109 -0
  4. package/dist/builtin-tools.js +200 -0
  5. package/dist/execute-call.d.ts +5 -0
  6. package/dist/execute-call.js +45 -0
  7. package/dist/index.d.ts +0 -7
  8. package/dist/index.js +7 -312
  9. package/dist/openapi-loader.d.ts +1 -14
  10. package/dist/openapi-loader.js +8 -49
  11. package/dist/state.d.ts +13 -0
  12. package/dist/state.js +54 -0
  13. package/dist/tool-schema.d.ts +2 -0
  14. package/dist/tool-schema.js +42 -0
  15. package/package.json +6 -6
  16. package/specs/production/accounting.json +2533 -1937
  17. package/specs/production/blog.json +1608 -1
  18. package/specs/production/broker.json +52 -24
  19. package/specs/production/chatgpt.json +299 -17
  20. package/specs/production/employees.json +3116 -278
  21. package/specs/production/events.json +437 -51
  22. package/specs/production/notifications.json +441 -11
  23. package/specs/production/office.json +818 -836
  24. package/specs/production/recruitment.json +3635 -1
  25. package/specs/production/sales.json +5250 -417
  26. package/specs/production/sanity.json +28588 -15500
  27. package/specs/production/sync.json +116 -20
  28. package/specs/production/webshop.json +38 -18
  29. package/specs/test/accounting.json +2533 -1937
  30. package/specs/test/blog.json +1608 -1
  31. package/specs/test/broker.json +52 -24
  32. package/specs/test/chatgpt.json +299 -17
  33. package/specs/test/employees.json +3116 -278
  34. package/specs/test/events.json +437 -51
  35. package/specs/test/notifications.json +441 -11
  36. package/specs/test/office.json +818 -836
  37. package/specs/test/recruitment.json +3635 -1
  38. package/specs/test/sales.json +5250 -417
  39. package/specs/test/sanity.json +28588 -15500
  40. package/specs/test/sync.json +116 -20
  41. package/specs/test/webshop.json +38 -18
package/dist/index.js CHANGED
@@ -1,193 +1,11 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * Snokam MCP Server
4
- *
5
- * Exposes Snokam backend APIs as MCP tools by reading bundled OpenAPI specs.
6
- * Auth is handled via @azure/identity — supports Azure CLI (local),
7
- * managed identity (Azure), and OBO (when SNOKAM_USER_JWT is set).
8
- */
9
2
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
10
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
11
4
  import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
12
- import { fetchSpecs, fetchSpecFromUrl, } from "./openapi-loader.js";
13
- import { getAccessToken } from "./auth.js";
14
- // ---------------------------------------------------------------------------
15
- // State
16
- // ---------------------------------------------------------------------------
17
- let currentEnvironment = process.env.SNOKAM_ENVIRONMENT ?? "production";
18
- let endpoints = [];
19
- let endpointsByTool = new Map();
20
- const serviceUrlOverrides = new Map();
21
- async function loadEndpoints(environment) {
22
- currentEnvironment = environment;
23
- endpoints = await fetchSpecs(environment);
24
- endpointsByTool = new Map();
25
- for (const ep of endpoints) {
26
- endpointsByTool.set(ep.toolName, ep);
27
- }
28
- // Re-apply any active URL overrides
29
- applyUrlOverrides();
30
- }
31
- function applyUrlOverrides() {
32
- for (const ep of endpoints) {
33
- const override = serviceUrlOverrides.get(ep.service);
34
- if (override) {
35
- ep.baseUrl = override;
36
- }
37
- }
38
- }
39
- // ---------------------------------------------------------------------------
40
- // Built-in tool: SwitchEnvironment
41
- // ---------------------------------------------------------------------------
42
- const SWITCH_TOOL_NAME = "SwitchEnvironment";
43
- const VALID_ENVIRONMENTS = ["production", "test"];
44
- const switchToolDef = {
45
- name: SWITCH_TOOL_NAME,
46
- description: "Switch the Snokam MCP server between environments (production/test). Reloads all API specs for the new environment.",
47
- inputSchema: {
48
- type: "object",
49
- properties: {
50
- environment: {
51
- type: "string",
52
- enum: VALID_ENVIRONMENTS,
53
- description: "The environment to switch to",
54
- },
55
- },
56
- required: ["environment"],
57
- },
58
- };
59
- // ---------------------------------------------------------------------------
60
- // Built-in tool: SetServiceUrl
61
- // ---------------------------------------------------------------------------
62
- const SET_URL_TOOL_NAME = "SetServiceUrl";
63
- const RESET_URL_TOOL_NAME = "ResetServiceUrl";
64
- const setUrlToolDef = {
65
- name: SET_URL_TOOL_NAME,
66
- description: "Override a service's base URL, e.g. to point to a locally running function. Auth is skipped for localhost URLs. Use ResetServiceUrl to revert.",
67
- inputSchema: {
68
- type: "object",
69
- properties: {
70
- service: {
71
- type: "string",
72
- description: "The service name (e.g. employees, notifications, events)",
73
- },
74
- url: {
75
- type: "string",
76
- description: "The base URL to use (e.g. http://localhost:7071)",
77
- },
78
- },
79
- required: ["service", "url"],
80
- },
81
- };
82
- const resetUrlToolDef = {
83
- name: RESET_URL_TOOL_NAME,
84
- description: "Reset a service's base URL back to the environment default. Call without arguments to reset all overrides.",
85
- inputSchema: {
86
- type: "object",
87
- properties: {
88
- service: {
89
- type: "string",
90
- description: "The service name to reset. Omit to reset all overrides.",
91
- },
92
- },
93
- },
94
- };
95
- // ---------------------------------------------------------------------------
96
- // JSON Schema builder for tool inputs
97
- // ---------------------------------------------------------------------------
98
- function buildInputSchema(endpoint) {
99
- const properties = {};
100
- const required = [];
101
- for (const param of endpoint.parameters) {
102
- const prop = {};
103
- if (param.schema?.type)
104
- prop.type = param.schema.type;
105
- if (param.schema?.enum)
106
- prop.enum = param.schema.enum;
107
- if (param.schema?.format)
108
- prop.format = param.schema.format;
109
- if (param.schema?.items)
110
- prop.items = param.schema.items;
111
- if (param.description)
112
- prop.description = param.description;
113
- if (!prop.type)
114
- prop.type = "string";
115
- properties[param.name] = prop;
116
- if (param.required)
117
- required.push(param.name);
118
- }
119
- if (endpoint.requestBody) {
120
- properties.body = {
121
- type: "object",
122
- description: endpoint.requestBody.description ?? "Request body",
123
- };
124
- // Extract schema from content type if available
125
- const jsonContent = endpoint.requestBody.content?.["application/json"];
126
- if (jsonContent?.schema) {
127
- properties.body = {
128
- ...properties.body,
129
- ...jsonContent.schema,
130
- };
131
- }
132
- if (endpoint.requestBody.required)
133
- required.push("body");
134
- }
135
- return {
136
- type: "object",
137
- properties,
138
- required: required.length > 0 ? required : undefined,
139
- };
140
- }
141
- // ---------------------------------------------------------------------------
142
- // HTTP call execution
143
- // ---------------------------------------------------------------------------
144
- async function executeCall(endpoint, args) {
145
- let url = `${endpoint.baseUrl}${endpoint.path}`;
146
- const queryParams = [];
147
- for (const param of endpoint.parameters) {
148
- const value = args[param.name];
149
- if (value === undefined)
150
- continue;
151
- if (param.in === "path") {
152
- url = url.replace(`{${param.name}}`, encodeURIComponent(String(value)));
153
- }
154
- else if (param.in === "query") {
155
- queryParams.push(`${encodeURIComponent(param.name)}=${encodeURIComponent(String(value))}`);
156
- }
157
- }
158
- if (queryParams.length > 0) {
159
- url += `?${queryParams.join("&")}`;
160
- }
161
- const headers = {
162
- Accept: "application/json",
163
- };
164
- const token = await getAccessToken(endpoint.scope);
165
- if (token) {
166
- headers.Authorization = `Bearer ${token}`;
167
- }
168
- let fetchBody;
169
- if (args.body !== undefined && endpoint.method !== "GET") {
170
- headers["Content-Type"] = "application/json";
171
- fetchBody = JSON.stringify(args.body);
172
- }
173
- const response = await fetch(url, {
174
- method: endpoint.method,
175
- headers,
176
- body: fetchBody,
177
- });
178
- let body;
179
- const contentType = response.headers.get("content-type") ?? "";
180
- if (contentType.includes("application/json")) {
181
- body = await response.json();
182
- }
183
- else {
184
- body = await response.text();
185
- }
186
- return { status: response.status, body };
187
- }
188
- // ---------------------------------------------------------------------------
189
- // Server setup
190
- // ---------------------------------------------------------------------------
5
+ import { BUILTIN_TOOL_DEFS, BUILTIN_TOOL_NAMES, handleBuiltinTool, } from "./builtin-tools.js";
6
+ import { executeCall } from "./execute-call.js";
7
+ import { currentEnvironment, endpoints, endpointsByTool, loadEndpoints, } from "./state.js";
8
+ import { buildInputSchema } from "./tool-schema.js";
191
9
  async function main() {
192
10
  await loadEndpoints(currentEnvironment);
193
11
  if (endpoints.length === 0) {
@@ -202,7 +20,6 @@ async function main() {
202
20
  resources: {},
203
21
  },
204
22
  });
205
- // List resources
206
23
  server.setRequestHandler(ListResourcesRequestSchema, async () => ({
207
24
  resources: [
208
25
  {
@@ -213,11 +30,9 @@ async function main() {
213
30
  },
214
31
  ],
215
32
  }));
216
- // Read resource
217
33
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
218
34
  const { uri } = request.params;
219
35
  if (uri === "snokam://about") {
220
- // Group endpoints by service with their descriptions
221
36
  const serviceMap = new Map();
222
37
  for (const ep of endpoints) {
223
38
  const existing = serviceMap.get(ep.service);
@@ -289,12 +104,9 @@ Controls: Sonos speakers, lights, YouTube queue
289
104
  isError: true,
290
105
  };
291
106
  });
292
- // List tools
293
107
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
294
108
  tools: [
295
- switchToolDef,
296
- setUrlToolDef,
297
- resetUrlToolDef,
109
+ ...BUILTIN_TOOL_DEFS,
298
110
  ...endpoints.map((ep) => ({
299
111
  name: ep.toolName,
300
112
  description: ep.description || ep.summary || `${ep.method} ${ep.path}`,
@@ -302,127 +114,10 @@ Controls: Sonos speakers, lights, YouTube queue
302
114
  })),
303
115
  ],
304
116
  }));
305
- // Call tool
306
117
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
307
118
  const { name, arguments: args = {} } = request.params;
308
- // Handle SwitchEnvironment
309
- if (name === SWITCH_TOOL_NAME) {
310
- const env = String(args.environment ?? "");
311
- if (!VALID_ENVIRONMENTS.includes(env)) {
312
- return {
313
- content: [
314
- {
315
- type: "text",
316
- text: `Invalid environment: ${env}. Must be one of: ${VALID_ENVIRONMENTS.join(", ")}`,
317
- },
318
- ],
319
- isError: true,
320
- };
321
- }
322
- if (env === currentEnvironment) {
323
- return {
324
- content: [
325
- {
326
- type: "text",
327
- text: `Already connected to ${env} (${endpoints.length} endpoints)`,
328
- },
329
- ],
330
- };
331
- }
332
- await loadEndpoints(env);
333
- // Notify client that the tool list has changed
334
- await server.sendToolListChanged();
335
- return {
336
- content: [
337
- {
338
- type: "text",
339
- text: `Switched to ${env} environment. Loaded ${endpoints.length} endpoints.`,
340
- },
341
- ],
342
- };
343
- }
344
- // Handle SetServiceUrl
345
- if (name === SET_URL_TOOL_NAME) {
346
- const service = String(args.service ?? "");
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
381
- const serviceEndpoints = endpoints.filter((ep) => ep.service === service);
382
- if (serviceEndpoints.length === 0) {
383
- const available = [
384
- ...new Set(endpoints.map((ep) => ep.service)),
385
- ].sort();
386
- return {
387
- content: [
388
- {
389
- type: "text",
390
- text: `Unknown service: ${service}. Available: ${available.join(", ")}`,
391
- },
392
- ],
393
- isError: true,
394
- };
395
- }
396
- serviceUrlOverrides.set(service, url);
397
- for (const ep of serviceEndpoints) {
398
- ep.baseUrl = url;
399
- }
400
- return {
401
- content: [
402
- {
403
- type: "text",
404
- text: `Overrode ${service} → ${url} (${serviceEndpoints.length} endpoints).`,
405
- },
406
- ],
407
- };
408
- }
409
- // Handle ResetServiceUrl
410
- if (name === RESET_URL_TOOL_NAME) {
411
- const service = args.service ? String(args.service) : undefined;
412
- if (service) {
413
- serviceUrlOverrides.delete(service);
414
- }
415
- else {
416
- serviceUrlOverrides.clear();
417
- }
418
- // Reload to restore original URLs
419
- await loadEndpoints(currentEnvironment);
420
- const msg = service
421
- ? `Reset ${service} to environment default`
422
- : `Reset all service URL overrides`;
423
- return {
424
- content: [{ type: "text", text: msg }],
425
- };
119
+ if (BUILTIN_TOOL_NAMES.has(name)) {
120
+ return handleBuiltinTool(server, name, args);
426
121
  }
427
122
  const endpoint = endpointsByTool.get(name);
428
123
  if (!endpoint) {
@@ -1,11 +1,3 @@
1
- /**
2
- * Fetch OpenAPI specs from live Snokam APIs and produce tool definitions.
3
- *
4
- * Each backend function is available at `{service}.api.snokam.no` (prod)
5
- * or `{service}.api.test.snokam.no` (test). The loader fetches `/swagger.json`
6
- * from each, extracts endpoints, parameters, and OAuth scopes to generate
7
- * MCP-compatible tool metadata.
8
- */
9
1
  export interface ApiEndpoint {
10
2
  service: string;
11
3
  serviceDescription: string;
@@ -18,7 +10,6 @@ export interface ApiEndpoint {
18
10
  description: string;
19
11
  parameters: OpenApiParameter[];
20
12
  requestBody: OpenApiRequestBody | null;
21
- /** OAuth2 scope in `.default` format for OBO exchange, or null for public endpoints. */
22
13
  scope: string | null;
23
14
  }
24
15
  interface OpenApiParameter {
@@ -42,10 +33,6 @@ interface OpenApiRequestBody {
42
33
  schema?: Record<string, unknown>;
43
34
  }>;
44
35
  }
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
36
  export declare function fetchSpecFromUrl(service: string, baseUrl: string): Promise<ApiEndpoint[]>;
50
- export declare function fetchSpecs(environment: string): Promise<ApiEndpoint[]>;
37
+ export declare function fetchSpecs(environment: string, apiHost: string): Promise<ApiEndpoint[]>;
51
38
  export {};
@@ -1,14 +1,3 @@
1
- /**
2
- * Fetch OpenAPI specs from live Snokam APIs and produce tool definitions.
3
- *
4
- * Each backend function is available at `{service}.api.snokam.no` (prod)
5
- * or `{service}.api.test.snokam.no` (test). The loader fetches `/swagger.json`
6
- * from each, extracts endpoints, parameters, and OAuth scopes to generate
7
- * MCP-compatible tool metadata.
8
- */
9
- // ---------------------------------------------------------------------------
10
- // Service discovery - reads from bundled specs directory at runtime
11
- // ---------------------------------------------------------------------------
12
1
  async function discoverServices(environment) {
13
2
  try {
14
3
  const { readdir } = await import("fs/promises");
@@ -23,24 +12,12 @@ async function discoverServices(environment) {
23
12
  .sort();
24
13
  }
25
14
  catch {
26
- // No bundled specs found - return empty array
27
15
  return [];
28
16
  }
29
17
  }
30
- // ---------------------------------------------------------------------------
31
- // Environment-aware URL resolution
32
- // ---------------------------------------------------------------------------
33
- const PROD_DOMAIN = "api.snokam.no";
34
- const TEST_DOMAIN = "api.test.snokam.no";
35
- function getBaseDomain(environment) {
36
- return environment === "test" ? TEST_DOMAIN : PROD_DOMAIN;
18
+ function getBaseUrl(service, apiHost) {
19
+ return `https://${apiHost}/api/${service}`;
37
20
  }
38
- function getBaseUrl(service, environment) {
39
- return `https://${service}.${getBaseDomain(environment)}`;
40
- }
41
- // ---------------------------------------------------------------------------
42
- // Scope extraction
43
- // ---------------------------------------------------------------------------
44
21
  function extractScope(operation) {
45
22
  for (const secReq of operation.security ?? []) {
46
23
  for (const scopes of Object.values(secReq)) {
@@ -56,15 +33,9 @@ function extractScope(operation) {
56
33
  }
57
34
  return null;
58
35
  }
59
- // ---------------------------------------------------------------------------
60
- // Tool naming
61
- // ---------------------------------------------------------------------------
62
36
  function makeToolName(service, operationId) {
63
37
  return `${service}__${operationId}`;
64
38
  }
65
- // ---------------------------------------------------------------------------
66
- // Spec parsing
67
- // ---------------------------------------------------------------------------
68
39
  function parseSpec(spec, service, baseUrl) {
69
40
  const endpoints = [];
70
41
  const paths = spec.paths;
@@ -98,13 +69,6 @@ function parseSpec(spec, service, baseUrl) {
98
69
  }
99
70
  return endpoints;
100
71
  }
101
- // ---------------------------------------------------------------------------
102
- // Public API
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
72
  export async function fetchSpecFromUrl(service, baseUrl) {
109
73
  const swaggerUrl = `${baseUrl}/swagger.json`;
110
74
  const response = await fetch(swaggerUrl, {
@@ -116,10 +80,9 @@ export async function fetchSpecFromUrl(service, baseUrl) {
116
80
  const spec = (await response.json());
117
81
  return parseSpec(spec, service, baseUrl);
118
82
  }
119
- export async function fetchSpecs(environment) {
120
- // Try to load bundled specs first (for speed)
83
+ export async function fetchSpecs(environment, apiHost) {
121
84
  try {
122
- const bundledSpecs = await loadBundledSpecs(environment);
85
+ const bundledSpecs = await loadBundledSpecs(environment, apiHost);
123
86
  if (bundledSpecs.length > 0) {
124
87
  console.error(`[snokam-mcp] Loaded ${bundledSpecs.length} endpoints from bundled specs (env=${environment})`);
125
88
  return bundledSpecs;
@@ -128,8 +91,6 @@ export async function fetchSpecs(environment) {
128
91
  catch (error) {
129
92
  console.error(`[snokam-mcp] Failed to load bundled specs, falling back to live fetch:`, error);
130
93
  }
131
- // Fallback to live fetching (slower, for development)
132
- // Discover services from environment to know which APIs to fetch
133
94
  const services = await discoverServices(environment);
134
95
  if (services.length === 0) {
135
96
  console.error("[snokam-mcp] No services discovered. Unable to fetch specs.");
@@ -137,7 +98,7 @@ export async function fetchSpecs(environment) {
137
98
  }
138
99
  const endpoints = [];
139
100
  const results = await Promise.allSettled(services.map(async (service) => {
140
- const baseUrl = getBaseUrl(service, environment);
101
+ const baseUrl = getBaseUrl(service, apiHost);
141
102
  const swaggerUrl = `${baseUrl}/swagger.json`;
142
103
  const response = await fetch(swaggerUrl, {
143
104
  signal: AbortSignal.timeout(10_000),
@@ -163,13 +124,12 @@ export async function fetchSpecs(environment) {
163
124
  console.error(`[snokam-mcp] Loaded ${endpoints.length} endpoints from ${successCount}/${services.length} services (env=${environment})`);
164
125
  return endpoints;
165
126
  }
166
- async function loadBundledSpecs(environment) {
127
+ async function loadBundledSpecs(environment, apiHost) {
167
128
  const { readFile } = await import("fs/promises");
168
129
  const { fileURLToPath } = await import("url");
169
130
  const { dirname, join } = await import("path");
170
131
  const __dirname = dirname(fileURLToPath(import.meta.url));
171
132
  const specsDir = join(__dirname, "..", "specs", environment);
172
- // Discover which specs are bundled
173
133
  const services = await discoverServices(environment);
174
134
  const endpoints = [];
175
135
  for (const service of services) {
@@ -177,11 +137,10 @@ async function loadBundledSpecs(environment) {
177
137
  const specPath = join(specsDir, `${service}.json`);
178
138
  const specData = await readFile(specPath, "utf-8");
179
139
  const spec = JSON.parse(specData);
180
- const baseUrl = getBaseUrl(service, environment);
140
+ const baseUrl = getBaseUrl(service, apiHost);
181
141
  endpoints.push(...parseSpec(spec, service, baseUrl));
182
142
  }
183
- catch (error) {
184
- // Spec file doesn't exist or is invalid, skip
143
+ catch {
185
144
  continue;
186
145
  }
187
146
  }
@@ -0,0 +1,13 @@
1
+ import { type ApiEndpoint } from "./openapi-loader.js";
2
+ export declare let currentEnvironment: string;
3
+ export declare let apiHostOverride: string | null;
4
+ export declare let endpoints: ApiEndpoint[];
5
+ export declare let endpointsByTool: Map<string, ApiEndpoint>;
6
+ export declare const serviceUrlOverrides: Map<string, string>;
7
+ export declare function setApiHostOverride(host: string | null): void;
8
+ export declare function resolveApiHost(environment: string): string;
9
+ export declare function activeApiHost(): string;
10
+ export declare function loadEndpoints(environment: string): Promise<void>;
11
+ export declare function normalizeDomain(raw: string): string;
12
+ export declare function rebuildEndpointsByTool(): void;
13
+ export declare function replaceServiceEndpoints(service: string, freshEndpoints: ApiEndpoint[]): void;
package/dist/state.js ADDED
@@ -0,0 +1,54 @@
1
+ import { fetchSpecs } from "./openapi-loader.js";
2
+ export let currentEnvironment = process.env.SNOKAM_ENVIRONMENT ?? "production";
3
+ export let apiHostOverride = null;
4
+ export let endpoints = [];
5
+ export let endpointsByTool = new Map();
6
+ export const serviceUrlOverrides = new Map();
7
+ export function setApiHostOverride(host) {
8
+ apiHostOverride = host;
9
+ }
10
+ export function resolveApiHost(environment) {
11
+ const explicit = process.env.SNOKAM_API_HOST;
12
+ if (explicit)
13
+ return normalizeDomain(explicit);
14
+ const suffix = environment === "test" ? "test.snosky.no" : "snosky.no";
15
+ const business = process.env.SNOKAM_BUSINESS;
16
+ return business ? `${business}.${suffix}` : suffix;
17
+ }
18
+ export function activeApiHost() {
19
+ return apiHostOverride ?? resolveApiHost(currentEnvironment);
20
+ }
21
+ export async function loadEndpoints(environment) {
22
+ currentEnvironment = environment;
23
+ endpoints = await fetchSpecs(environment, activeApiHost());
24
+ endpointsByTool = new Map();
25
+ for (const ep of endpoints) {
26
+ endpointsByTool.set(ep.toolName, ep);
27
+ }
28
+ applyUrlOverrides();
29
+ }
30
+ function applyUrlOverrides() {
31
+ for (const ep of endpoints) {
32
+ const override = serviceUrlOverrides.get(ep.service);
33
+ if (override) {
34
+ ep.baseUrl = override;
35
+ }
36
+ }
37
+ }
38
+ export function normalizeDomain(raw) {
39
+ return raw
40
+ .trim()
41
+ .replace(/^https?:\/\//, "")
42
+ .replace(/\/$/, "");
43
+ }
44
+ export function rebuildEndpointsByTool() {
45
+ endpointsByTool = new Map();
46
+ for (const ep of endpoints) {
47
+ endpointsByTool.set(ep.toolName, ep);
48
+ }
49
+ }
50
+ export function replaceServiceEndpoints(service, freshEndpoints) {
51
+ endpoints = endpoints.filter((ep) => ep.service !== service);
52
+ endpoints.push(...freshEndpoints);
53
+ rebuildEndpointsByTool();
54
+ }
@@ -0,0 +1,2 @@
1
+ import { type ApiEndpoint } from "./openapi-loader.js";
2
+ export declare function buildInputSchema(endpoint: ApiEndpoint): Record<string, unknown>;
@@ -0,0 +1,42 @@
1
+ export function buildInputSchema(endpoint) {
2
+ const properties = {};
3
+ const required = [];
4
+ for (const param of endpoint.parameters) {
5
+ const prop = {};
6
+ if (param.schema?.type)
7
+ prop.type = param.schema.type;
8
+ if (param.schema?.enum)
9
+ prop.enum = param.schema.enum;
10
+ if (param.schema?.format)
11
+ prop.format = param.schema.format;
12
+ if (param.schema?.items)
13
+ prop.items = param.schema.items;
14
+ if (param.description)
15
+ prop.description = param.description;
16
+ if (!prop.type)
17
+ prop.type = "string";
18
+ properties[param.name] = prop;
19
+ if (param.required)
20
+ required.push(param.name);
21
+ }
22
+ if (endpoint.requestBody) {
23
+ properties.body = {
24
+ type: "object",
25
+ description: endpoint.requestBody.description ?? "Request body",
26
+ };
27
+ const jsonContent = endpoint.requestBody.content?.["application/json"];
28
+ if (jsonContent?.schema) {
29
+ properties.body = {
30
+ ...properties.body,
31
+ ...jsonContent.schema,
32
+ };
33
+ }
34
+ if (endpoint.requestBody.required)
35
+ required.push("body");
36
+ }
37
+ return {
38
+ type: "object",
39
+ properties,
40
+ required: required.length > 0 ? required : undefined,
41
+ };
42
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snokam/mcp-api",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
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": {
@@ -21,13 +21,13 @@
21
21
  "specs"
22
22
  ],
23
23
  "dependencies": {
24
- "@azure/identity": "^4.6.0",
25
- "@modelcontextprotocol/sdk": "^1.12.1",
26
- "zod": "^3.24.4"
24
+ "@azure/identity": "^4.13.1",
25
+ "@modelcontextprotocol/sdk": "^1.29.0",
26
+ "zod": "^4.4.3"
27
27
  },
28
28
  "devDependencies": {
29
- "@types/node": "^22.15.0",
30
- "typescript": "~5.8.3"
29
+ "@types/node": "^26.1.1",
30
+ "typescript": "~6.0.2"
31
31
  },
32
32
  "publishConfig": {
33
33
  "access": "public"