@snokam/mcp-api 1.21.1 → 2.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.
Files changed (43) hide show
  1. package/dist/auth.d.ts +0 -15
  2. package/dist/auth.js +17 -61
  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 +16 -311
  9. package/dist/openapi-loader.d.ts +1 -14
  10. package/dist/openapi-loader.js +27 -50
  11. package/dist/requirements.d.ts +10 -0
  12. package/dist/requirements.js +92 -0
  13. package/dist/state.d.ts +14 -0
  14. package/dist/state.js +66 -0
  15. package/dist/tool-schema.d.ts +2 -0
  16. package/dist/tool-schema.js +42 -0
  17. package/package.json +3 -3
  18. package/specs/production/accounting.json +2629 -1754
  19. package/specs/production/blog.json +1608 -1
  20. package/specs/production/broker.json +52 -24
  21. package/specs/production/chatgpt.json +299 -17
  22. package/specs/production/employees.json +2979 -299
  23. package/specs/production/events.json +366 -52
  24. package/specs/production/notifications.json +390 -13
  25. package/specs/production/office.json +818 -836
  26. package/specs/production/recruitment.json +3635 -1
  27. package/specs/production/sales.json +5088 -393
  28. package/specs/production/sanity.json +29008 -15539
  29. package/specs/production/sync.json +116 -20
  30. package/specs/production/webshop.json +38 -18
  31. package/specs/test/accounting.json +2629 -1754
  32. package/specs/test/blog.json +1608 -1
  33. package/specs/test/broker.json +52 -24
  34. package/specs/test/chatgpt.json +299 -17
  35. package/specs/test/employees.json +2979 -299
  36. package/specs/test/events.json +366 -52
  37. package/specs/test/notifications.json +390 -13
  38. package/specs/test/office.json +818 -836
  39. package/specs/test/recruitment.json +3635 -1
  40. package/specs/test/sales.json +5088 -393
  41. package/specs/test/sanity.json +29008 -15539
  42. package/specs/test/sync.json +116 -20
  43. package/specs/test/webshop.json +38 -18
package/dist/index.js CHANGED
@@ -1,194 +1,15 @@
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 { settled } from "./state.js";
8
+ import { currentEnvironment, endpoints, endpointsByTool, loadEndpoints, } from "./state.js";
9
+ import { buildInputSchema } from "./tool-schema.js";
10
+ import { blockedReason, reportRequirements } from "./requirements.js";
191
11
  async function main() {
12
+ reportRequirements();
192
13
  await loadEndpoints(currentEnvironment);
193
14
  if (endpoints.length === 0) {
194
15
  console.error("[snokam-mcp] No endpoints loaded. Ensure specs/*.json files exist.");
@@ -202,7 +23,6 @@ async function main() {
202
23
  resources: {},
203
24
  },
204
25
  });
205
- // List resources
206
26
  server.setRequestHandler(ListResourcesRequestSchema, async () => ({
207
27
  resources: [
208
28
  {
@@ -213,11 +33,9 @@ async function main() {
213
33
  },
214
34
  ],
215
35
  }));
216
- // Read resource
217
36
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
218
37
  const { uri } = request.params;
219
38
  if (uri === "snokam://about") {
220
- // Group endpoints by service with their descriptions
221
39
  const serviceMap = new Map();
222
40
  for (const ep of endpoints) {
223
41
  const existing = serviceMap.get(ep.service);
@@ -289,12 +107,9 @@ Controls: Sonos speakers, lights, YouTube queue
289
107
  isError: true,
290
108
  };
291
109
  });
292
- // List tools
293
110
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
294
111
  tools: [
295
- switchToolDef,
296
- setUrlToolDef,
297
- resetUrlToolDef,
112
+ ...BUILTIN_TOOL_DEFS,
298
113
  ...endpoints.map((ep) => ({
299
114
  name: ep.toolName,
300
115
  description: ep.description || ep.summary || `${ep.method} ${ep.path}`,
@@ -302,127 +117,17 @@ Controls: Sonos speakers, lights, YouTube queue
302
117
  })),
303
118
  ],
304
119
  }));
305
- // Call tool
306
120
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
307
121
  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
- };
122
+ if (BUILTIN_TOOL_NAMES.has(name)) {
123
+ const result = await handleBuiltinTool(server, name, args);
124
+ reportRequirements();
125
+ return result;
343
126
  }
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
- };
127
+ await settled();
128
+ const blocked = blockedReason();
129
+ if (blocked) {
130
+ return { content: [{ type: "text", text: blocked }], isError: true };
426
131
  }
427
132
  const endpoint = endpointsByTool.get(name);
428
133
  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,27 @@ 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
- // ---------------------------------------------------------------------------
39
+ function resourceOf(path) {
40
+ const segment = path
41
+ .split("/")
42
+ .find((part) => part && !part.startsWith("{") && !/^v[0-9]/.test(part));
43
+ return segment ? segment.replace(/[^a-zA-Z0-9]/g, "") : "";
44
+ }
45
+ function disambiguate(endpoints) {
46
+ const counts = new Map();
47
+ for (const ep of endpoints) {
48
+ counts.set(ep.toolName, (counts.get(ep.toolName) ?? 0) + 1);
49
+ }
50
+ return endpoints.map((ep) => {
51
+ if ((counts.get(ep.toolName) ?? 0) < 2)
52
+ return ep;
53
+ const resource = resourceOf(ep.path);
54
+ return resource ? { ...ep, toolName: `${ep.toolName}_${resource}` } : ep;
55
+ });
56
+ }
68
57
  function parseSpec(spec, service, baseUrl) {
69
58
  const endpoints = [];
70
59
  const paths = spec.paths;
@@ -96,15 +85,8 @@ function parseSpec(spec, service, baseUrl) {
96
85
  });
97
86
  }
98
87
  }
99
- return endpoints;
88
+ return disambiguate(endpoints);
100
89
  }
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
90
  export async function fetchSpecFromUrl(service, baseUrl) {
109
91
  const swaggerUrl = `${baseUrl}/swagger.json`;
110
92
  const response = await fetch(swaggerUrl, {
@@ -116,10 +98,9 @@ export async function fetchSpecFromUrl(service, baseUrl) {
116
98
  const spec = (await response.json());
117
99
  return parseSpec(spec, service, baseUrl);
118
100
  }
119
- export async function fetchSpecs(environment) {
120
- // Try to load bundled specs first (for speed)
101
+ export async function fetchSpecs(environment, apiHost) {
121
102
  try {
122
- const bundledSpecs = await loadBundledSpecs(environment);
103
+ const bundledSpecs = await loadBundledSpecs(environment, apiHost);
123
104
  if (bundledSpecs.length > 0) {
124
105
  console.error(`[snokam-mcp] Loaded ${bundledSpecs.length} endpoints from bundled specs (env=${environment})`);
125
106
  return bundledSpecs;
@@ -128,8 +109,6 @@ export async function fetchSpecs(environment) {
128
109
  catch (error) {
129
110
  console.error(`[snokam-mcp] Failed to load bundled specs, falling back to live fetch:`, error);
130
111
  }
131
- // Fallback to live fetching (slower, for development)
132
- // Discover services from environment to know which APIs to fetch
133
112
  const services = await discoverServices(environment);
134
113
  if (services.length === 0) {
135
114
  console.error("[snokam-mcp] No services discovered. Unable to fetch specs.");
@@ -137,7 +116,7 @@ export async function fetchSpecs(environment) {
137
116
  }
138
117
  const endpoints = [];
139
118
  const results = await Promise.allSettled(services.map(async (service) => {
140
- const baseUrl = getBaseUrl(service, environment);
119
+ const baseUrl = getBaseUrl(service, apiHost);
141
120
  const swaggerUrl = `${baseUrl}/swagger.json`;
142
121
  const response = await fetch(swaggerUrl, {
143
122
  signal: AbortSignal.timeout(10_000),
@@ -163,13 +142,12 @@ export async function fetchSpecs(environment) {
163
142
  console.error(`[snokam-mcp] Loaded ${endpoints.length} endpoints from ${successCount}/${services.length} services (env=${environment})`);
164
143
  return endpoints;
165
144
  }
166
- async function loadBundledSpecs(environment) {
145
+ async function loadBundledSpecs(environment, apiHost) {
167
146
  const { readFile } = await import("fs/promises");
168
147
  const { fileURLToPath } = await import("url");
169
148
  const { dirname, join } = await import("path");
170
149
  const __dirname = dirname(fileURLToPath(import.meta.url));
171
150
  const specsDir = join(__dirname, "..", "specs", environment);
172
- // Discover which specs are bundled
173
151
  const services = await discoverServices(environment);
174
152
  const endpoints = [];
175
153
  for (const service of services) {
@@ -177,11 +155,10 @@ async function loadBundledSpecs(environment) {
177
155
  const specPath = join(specsDir, `${service}.json`);
178
156
  const specData = await readFile(specPath, "utf-8");
179
157
  const spec = JSON.parse(specData);
180
- const baseUrl = getBaseUrl(service, environment);
158
+ const baseUrl = getBaseUrl(service, apiHost);
181
159
  endpoints.push(...parseSpec(spec, service, baseUrl));
182
160
  }
183
- catch (error) {
184
- // Spec file doesn't exist or is invalid, skip
161
+ catch {
185
162
  continue;
186
163
  }
187
164
  }
@@ -0,0 +1,10 @@
1
+ export interface Requirement {
2
+ name: string;
3
+ ok: boolean;
4
+ detail: string;
5
+ fix?: string;
6
+ }
7
+ export declare function checkRequirements(): Requirement[];
8
+ export declare function unmetRequirements(): Requirement[];
9
+ export declare function reportRequirements(): Requirement[];
10
+ export declare function blockedReason(): string | null;
@@ -0,0 +1,92 @@
1
+ import { activeApiHost, apiHostOverride, currentEnvironment } from "./state.js";
2
+ import { SWITCH_DOMAIN_TOOL_NAME, SWITCH_TOOL_NAME, VALID_ENVIRONMENTS, } from "./builtin-tools.js";
3
+ const BUSINESS_HOST = /^[a-z0-9-]+\.(test\.)?snosky\.no$/;
4
+ function hostRequirement() {
5
+ const host = activeApiHost();
6
+ const explicit = process.env.SNOKAM_API_HOST?.trim();
7
+ const business = process.env.SNOKAM_BUSINESS?.trim();
8
+ if (apiHostOverride) {
9
+ return {
10
+ name: "api host",
11
+ ok: true,
12
+ detail: `${apiHostOverride} (set with ${SWITCH_DOMAIN_TOOL_NAME})`,
13
+ };
14
+ }
15
+ if (explicit) {
16
+ return { name: "api host", ok: true, detail: `${host} (SNOKAM_API_HOST)` };
17
+ }
18
+ if (business) {
19
+ return {
20
+ name: "api host",
21
+ ok: BUSINESS_HOST.test(host),
22
+ detail: `${host} (SNOKAM_BUSINESS=${business})`,
23
+ fix: BUSINESS_HOST.test(host)
24
+ ? undefined
25
+ : `"${business}" does not produce a business host. Call ${SWITCH_DOMAIN_TOOL_NAME} with the host itself, e.g. acme.snosky.no.`,
26
+ };
27
+ }
28
+ return {
29
+ name: "api host",
30
+ ok: false,
31
+ detail: `${host} — no business chosen`,
32
+ fix: `${host} is the marketing site, not a business API host: requests reach the frontend and come back as its 404 page, which is indistinguishable from a missing document. Call ${SWITCH_DOMAIN_TOOL_NAME} with the business host (e.g. snokam.snosky.no), or start with SNOKAM_BUSINESS=snokam.`,
33
+ };
34
+ }
35
+ function environmentRequirement() {
36
+ const ok = VALID_ENVIRONMENTS.includes(currentEnvironment);
37
+ return {
38
+ name: "environment",
39
+ ok,
40
+ detail: currentEnvironment,
41
+ fix: ok
42
+ ? undefined
43
+ : `Expected one of ${VALID_ENVIRONMENTS.join(", ")}. Call ${SWITCH_TOOL_NAME}, or set SNOKAM_ENVIRONMENT.`,
44
+ };
45
+ }
46
+ function credentialsRequirement() {
47
+ if (process.env.SNOKAM_ACCESS_TOKEN) {
48
+ return { name: "credentials", ok: true, detail: "SNOKAM_ACCESS_TOKEN" };
49
+ }
50
+ if (process.env.SNOKAM_ACCESS_TOKEN_FILE) {
51
+ return {
52
+ name: "credentials",
53
+ ok: true,
54
+ detail: "SNOKAM_ACCESS_TOKEN_FILE",
55
+ };
56
+ }
57
+ return {
58
+ name: "credentials",
59
+ ok: true,
60
+ detail: "Azure credentials (az login). Calls needing a token fail with 401 if none can be minted.",
61
+ };
62
+ }
63
+ export function checkRequirements() {
64
+ return [
65
+ hostRequirement(),
66
+ environmentRequirement(),
67
+ credentialsRequirement(),
68
+ ];
69
+ }
70
+ export function unmetRequirements() {
71
+ return checkRequirements().filter((requirement) => !requirement.ok);
72
+ }
73
+ export function reportRequirements() {
74
+ const requirements = checkRequirements();
75
+ for (const requirement of requirements) {
76
+ console.error(`[snokam-mcp] ${requirement.ok ? "✔" : "✖"} ${requirement.name}: ${requirement.detail}`);
77
+ if (requirement.fix)
78
+ console.error(`[snokam-mcp] ${requirement.fix}`);
79
+ }
80
+ return requirements;
81
+ }
82
+ export function blockedReason() {
83
+ const unmet = unmetRequirements();
84
+ if (unmet.length === 0)
85
+ return null;
86
+ return [
87
+ "This call was not sent, because the server is not in a state where it could succeed:",
88
+ ...unmet.map((r) => ` ✖ ${r.name}: ${r.detail}\n ${r.fix ?? ""}`.trimEnd()),
89
+ "",
90
+ "Fix the above, then call this tool again. Nothing else has changed.",
91
+ ].join("\n");
92
+ }
@@ -0,0 +1,14 @@
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 settled(): Promise<void>;
11
+ export declare function loadEndpoints(environment: string): Promise<void>;
12
+ export declare function normalizeDomain(raw: string): string;
13
+ export declare function rebuildEndpointsByTool(): void;
14
+ export declare function replaceServiceEndpoints(service: string, freshEndpoints: ApiEndpoint[]): void;