@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 +23 -18
- package/dist/openapi-loader.d.ts +7 -6
- package/dist/openapi-loader.js +13 -17
- package/package.json +1 -1
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
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
386
|
-
? `API calls now target ${
|
|
387
|
-
:
|
|
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
|
};
|
package/dist/openapi-loader.d.ts
CHANGED
|
@@ -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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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 {};
|
package/dist/openapi-loader.js
CHANGED
|
@@ -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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
|
|
35
|
-
|
|
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,
|
|
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,
|
|
177
|
+
const baseUrl = getBaseUrl(service, apiHost);
|
|
182
178
|
endpoints.push(...parseSpec(spec, service, baseUrl));
|
|
183
179
|
}
|
|
184
180
|
catch (error) {
|