@snokam/mcp-api 0.111.0 → 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 environment default (snokam).",
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 snokam 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,10 +1,12 @@
1
1
  /**
2
2
  * Fetch OpenAPI specs from live Snokam APIs and produce tool definitions.
3
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.
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.
8
10
  */
9
11
  export interface ApiEndpoint {
10
12
  service: string;
@@ -47,5 +49,5 @@ interface OpenApiRequestBody {
47
49
  * and return parsed endpoints for that service.
48
50
  */
49
51
  export declare function fetchSpecFromUrl(service: string, baseUrl: string): Promise<ApiEndpoint[]>;
50
- export declare function fetchSpecs(environment: string): Promise<ApiEndpoint[]>;
52
+ export declare function fetchSpecs(environment: string, apiHost: string): Promise<ApiEndpoint[]>;
51
53
  export {};
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * Fetch OpenAPI specs from live Snokam APIs and produce tool definitions.
3
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.
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.
8
10
  */
9
11
  // ---------------------------------------------------------------------------
10
12
  // Service discovery - reads from bundled specs directory at runtime
@@ -30,13 +32,8 @@ async function discoverServices(environment) {
30
32
  // ---------------------------------------------------------------------------
31
33
  // Environment-aware URL resolution
32
34
  // ---------------------------------------------------------------------------
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;
37
- }
38
- function getBaseUrl(service, environment) {
39
- return `https://${service}.${getBaseDomain(environment)}`;
35
+ function getBaseUrl(service, apiHost) {
36
+ return `https://${apiHost}/api/${service}`;
40
37
  }
41
38
  // ---------------------------------------------------------------------------
42
39
  // Scope extraction
@@ -116,10 +113,10 @@ export async function fetchSpecFromUrl(service, baseUrl) {
116
113
  const spec = (await response.json());
117
114
  return parseSpec(spec, service, baseUrl);
118
115
  }
119
- export async function fetchSpecs(environment) {
116
+ export async function fetchSpecs(environment, apiHost) {
120
117
  // Try to load bundled specs first (for speed)
121
118
  try {
122
- const bundledSpecs = await loadBundledSpecs(environment);
119
+ const bundledSpecs = await loadBundledSpecs(environment, apiHost);
123
120
  if (bundledSpecs.length > 0) {
124
121
  console.error(`[snokam-mcp] Loaded ${bundledSpecs.length} endpoints from bundled specs (env=${environment})`);
125
122
  return bundledSpecs;
@@ -137,7 +134,7 @@ export async function fetchSpecs(environment) {
137
134
  }
138
135
  const endpoints = [];
139
136
  const results = await Promise.allSettled(services.map(async (service) => {
140
- const baseUrl = getBaseUrl(service, environment);
137
+ const baseUrl = getBaseUrl(service, apiHost);
141
138
  const swaggerUrl = `${baseUrl}/swagger.json`;
142
139
  const response = await fetch(swaggerUrl, {
143
140
  signal: AbortSignal.timeout(10_000),
@@ -163,7 +160,7 @@ export async function fetchSpecs(environment) {
163
160
  console.error(`[snokam-mcp] Loaded ${endpoints.length} endpoints from ${successCount}/${services.length} services (env=${environment})`);
164
161
  return endpoints;
165
162
  }
166
- async function loadBundledSpecs(environment) {
163
+ async function loadBundledSpecs(environment, apiHost) {
167
164
  const { readFile } = await import("fs/promises");
168
165
  const { fileURLToPath } = await import("url");
169
166
  const { dirname, join } = await import("path");
@@ -177,7 +174,7 @@ async function loadBundledSpecs(environment) {
177
174
  const specPath = join(specsDir, `${service}.json`);
178
175
  const specData = await readFile(specPath, "utf-8");
179
176
  const spec = JSON.parse(specData);
180
- const baseUrl = getBaseUrl(service, environment);
177
+ const baseUrl = getBaseUrl(service, apiHost);
181
178
  endpoints.push(...parseSpec(spec, service, baseUrl));
182
179
  }
183
180
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snokam/mcp-api",
3
- "version": "0.111.0",
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": {