@snokam/mcp-api 0.111.1 → 0.111.2

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
@@ -15,29 +15,34 @@ import { getAccessToken } from "./auth.js";
15
15
  // State
16
16
  // ---------------------------------------------------------------------------
17
17
  let currentEnvironment = process.env.SNOKAM_ENVIRONMENT ?? "production";
18
- let currentApiDomain = null;
18
+ let apiHostOverride = null;
19
19
  let endpoints = [];
20
20
  let endpointsByTool = new Map();
21
21
  const serviceUrlOverrides = new Map();
22
+ // Backends are reached at {host}/api/{service} on the business's snosky host.
23
+ // SNOKAM_API_HOST sets it explicitly (injected per-business), else it's derived
24
+ // from SNOKAM_BUSINESS + env, and SwitchDomain overrides it at runtime.
25
+ function resolveApiHost(environment) {
26
+ const explicit = process.env.SNOKAM_API_HOST;
27
+ if (explicit)
28
+ return normalizeDomain(explicit);
29
+ const suffix = environment === "test" ? "test.snosky.no" : "snosky.no";
30
+ const business = process.env.SNOKAM_BUSINESS;
31
+ return business ? `${business}.${suffix}` : suffix;
32
+ }
33
+ function activeApiHost() {
34
+ return apiHostOverride ?? resolveApiHost(currentEnvironment);
35
+ }
22
36
  async function loadEndpoints(environment) {
23
37
  currentEnvironment = environment;
24
- endpoints = await fetchSpecs(environment);
38
+ endpoints = await fetchSpecs(environment, activeApiHost());
25
39
  endpointsByTool = new Map();
26
40
  for (const ep of endpoints) {
27
41
  endpointsByTool.set(ep.toolName, ep);
28
42
  }
29
- // Re-target every service at the active business domain (if set), then layer
30
- // per-service overrides on top (most specific wins).
31
- applyDomainOverride();
43
+ // Layer per-service overrides (e.g. localhost) on top; most specific wins.
32
44
  applyUrlOverrides();
33
45
  }
34
- function applyDomainOverride() {
35
- if (!currentApiDomain)
36
- return;
37
- for (const ep of endpoints) {
38
- ep.baseUrl = `https://${ep.service}.${currentApiDomain}`;
39
- }
40
- }
41
46
  function applyUrlOverrides() {
42
47
  for (const ep of endpoints) {
43
48
  const override = serviceUrlOverrides.get(ep.service);
@@ -111,13 +116,13 @@ const resetUrlToolDef = {
111
116
  const SWITCH_DOMAIN_TOOL_NAME = "SwitchDomain";
112
117
  const switchDomainToolDef = {
113
118
  name: SWITCH_DOMAIN_TOOL_NAME,
114
- description: "Point all Snokam API calls at a specific business's API domain, so the same MCP can serve any tenant. Pass the business's API domain (custom, e.g. api.acme.com, or its snosky fallback, e.g. api.acme.snosky.no); calls then go to {service}.{domain}. Pass an empty string to reset to the platform-wide snosky default.",
119
+ description: "Point all Snokam API calls at a specific business's API host, so the same MCP can serve any tenant. Pass the business's API host (custom, e.g. acme.no, or its snosky fallback, e.g. acme.snosky.no); calls then go to {host}/api/{service}. Pass an empty string to reset to the environment default.",
115
120
  inputSchema: {
116
121
  type: "object",
117
122
  properties: {
118
123
  domain: {
119
124
  type: "string",
120
- description: "The business's API base domain (e.g. api.acme.com). Empty string resets to the platform-wide snosky default.",
125
+ description: "The business's API host (e.g. acme.snosky.no). Empty string resets to the environment default.",
121
126
  },
122
127
  },
123
128
  required: ["domain"],
@@ -375,16 +380,16 @@ Controls: Sonos speakers, lights, YouTube queue
375
380
  }
376
381
  // Handle SwitchDomain
377
382
  if (name === SWITCH_DOMAIN_TOOL_NAME) {
378
- currentApiDomain = normalizeDomain(String(args.domain ?? "")) || null;
383
+ apiHostOverride = normalizeDomain(String(args.domain ?? "")) || null;
379
384
  await loadEndpoints(currentEnvironment);
380
385
  await server.sendToolListChanged();
381
386
  return {
382
387
  content: [
383
388
  {
384
389
  type: "text",
385
- text: currentApiDomain
386
- ? `API calls now target ${currentApiDomain} ({service}.${currentApiDomain}).`
387
- : "API domain reset to the environment default.",
390
+ text: apiHostOverride
391
+ ? `API calls now target ${apiHostOverride}/api/{service}.`
392
+ : `API host reset to the environment default (${activeApiHost()}).`,
388
393
  },
389
394
  ],
390
395
  };
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * Fetch OpenAPI specs from live Snokam APIs and produce tool definitions.
3
3
  *
4
- * Backends are reached on the platform-wide snosky domain, which is uniform
5
- * across every business (no per-customer hostname): `{service}.api.snosky.no`
6
- * (prod) or `{service}.api.test.snosky.no` (test). A specific business domain
7
- * can still be targeted at runtime via the SwitchDomain tool. The loader
8
- * extracts endpoints, parameters, and OAuth scopes for MCP tool metadata.
4
+ * Backends are reached per-business at `{business}.snosky.no/api/{service}`
5
+ * (prod) or `{business}.test.snosky.no/api/{service}` (test) — the uniform
6
+ * snosky routing that works for every tenant, no per-customer hostname. The
7
+ * business API host is injected (SNOKAM_API_HOST) and can be retargeted at
8
+ * runtime via the SwitchDomain tool. The loader extracts endpoints,
9
+ * parameters, and OAuth scopes for MCP tool metadata.
9
10
  */
10
11
  export interface ApiEndpoint {
11
12
  service: string;
@@ -48,5 +49,5 @@ interface OpenApiRequestBody {
48
49
  * and return parsed endpoints for that service.
49
50
  */
50
51
  export declare function fetchSpecFromUrl(service: string, baseUrl: string): Promise<ApiEndpoint[]>;
51
- export declare function fetchSpecs(environment: string): Promise<ApiEndpoint[]>;
52
+ export declare function fetchSpecs(environment: string, apiHost: string): Promise<ApiEndpoint[]>;
52
53
  export {};
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * Fetch OpenAPI specs from live Snokam APIs and produce tool definitions.
3
3
  *
4
- * Backends are reached on the platform-wide snosky domain, which is uniform
5
- * across every business (no per-customer hostname): `{service}.api.snosky.no`
6
- * (prod) or `{service}.api.test.snosky.no` (test). A specific business domain
7
- * can still be targeted at runtime via the SwitchDomain tool. The loader
8
- * extracts endpoints, parameters, and OAuth scopes for MCP tool metadata.
4
+ * Backends are reached per-business at `{business}.snosky.no/api/{service}`
5
+ * (prod) or `{business}.test.snosky.no/api/{service}` (test) — the uniform
6
+ * snosky routing that works for every tenant, no per-customer hostname. The
7
+ * business API host is injected (SNOKAM_API_HOST) and can be retargeted at
8
+ * runtime via the SwitchDomain tool. The loader extracts endpoints,
9
+ * parameters, and OAuth scopes for MCP tool metadata.
9
10
  */
10
11
  // ---------------------------------------------------------------------------
11
12
  // Service discovery - reads from bundled specs directory at runtime
@@ -31,13 +32,8 @@ async function discoverServices(environment) {
31
32
  // ---------------------------------------------------------------------------
32
33
  // Environment-aware URL resolution
33
34
  // ---------------------------------------------------------------------------
34
- const PROD_DOMAIN = "api.snosky.no";
35
- const TEST_DOMAIN = "api.test.snosky.no";
36
- function getBaseDomain(environment) {
37
- return environment === "test" ? TEST_DOMAIN : PROD_DOMAIN;
38
- }
39
- function getBaseUrl(service, environment) {
40
- return `https://${service}.${getBaseDomain(environment)}`;
35
+ function getBaseUrl(service, apiHost) {
36
+ return `https://${apiHost}/api/${service}`;
41
37
  }
42
38
  // ---------------------------------------------------------------------------
43
39
  // Scope extraction
@@ -117,10 +113,10 @@ export async function fetchSpecFromUrl(service, baseUrl) {
117
113
  const spec = (await response.json());
118
114
  return parseSpec(spec, service, baseUrl);
119
115
  }
120
- export async function fetchSpecs(environment) {
116
+ export async function fetchSpecs(environment, apiHost) {
121
117
  // Try to load bundled specs first (for speed)
122
118
  try {
123
- const bundledSpecs = await loadBundledSpecs(environment);
119
+ const bundledSpecs = await loadBundledSpecs(environment, apiHost);
124
120
  if (bundledSpecs.length > 0) {
125
121
  console.error(`[snokam-mcp] Loaded ${bundledSpecs.length} endpoints from bundled specs (env=${environment})`);
126
122
  return bundledSpecs;
@@ -138,7 +134,7 @@ export async function fetchSpecs(environment) {
138
134
  }
139
135
  const endpoints = [];
140
136
  const results = await Promise.allSettled(services.map(async (service) => {
141
- const baseUrl = getBaseUrl(service, environment);
137
+ const baseUrl = getBaseUrl(service, apiHost);
142
138
  const swaggerUrl = `${baseUrl}/swagger.json`;
143
139
  const response = await fetch(swaggerUrl, {
144
140
  signal: AbortSignal.timeout(10_000),
@@ -164,7 +160,7 @@ export async function fetchSpecs(environment) {
164
160
  console.error(`[snokam-mcp] Loaded ${endpoints.length} endpoints from ${successCount}/${services.length} services (env=${environment})`);
165
161
  return endpoints;
166
162
  }
167
- async function loadBundledSpecs(environment) {
163
+ async function loadBundledSpecs(environment, apiHost) {
168
164
  const { readFile } = await import("fs/promises");
169
165
  const { fileURLToPath } = await import("url");
170
166
  const { dirname, join } = await import("path");
@@ -178,7 +174,7 @@ async function loadBundledSpecs(environment) {
178
174
  const specPath = join(specsDir, `${service}.json`);
179
175
  const specData = await readFile(specPath, "utf-8");
180
176
  const spec = JSON.parse(specData);
181
- const baseUrl = getBaseUrl(service, environment);
177
+ const baseUrl = getBaseUrl(service, apiHost);
182
178
  endpoints.push(...parseSpec(spec, service, baseUrl));
183
179
  }
184
180
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snokam/mcp-api",
3
- "version": "0.111.1",
3
+ "version": "0.111.2",
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": {