@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 +23 -18
- package/dist/openapi-loader.d.ts +7 -5
- package/dist/openapi-loader.js +13 -16
- 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,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Fetch OpenAPI specs from live Snokam APIs and produce tool definitions.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* or `{
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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 {};
|
package/dist/openapi-loader.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Fetch OpenAPI specs from live Snokam APIs and produce tool definitions.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* or `{
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
|
|
34
|
-
|
|
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,
|
|
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,
|
|
177
|
+
const baseUrl = getBaseUrl(service, apiHost);
|
|
181
178
|
endpoints.push(...parseSpec(spec, service, baseUrl));
|
|
182
179
|
}
|
|
183
180
|
catch (error) {
|