@snokam/mcp-api 0.5.1 → 0.7.0
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 +184 -17
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -12,9 +12,86 @@ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSche
|
|
|
12
12
|
import { fetchSpecs } from "./openapi-loader.js";
|
|
13
13
|
import { getAccessToken } from "./auth.js";
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
|
-
//
|
|
15
|
+
// State
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
|
-
|
|
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
|
+
};
|
|
18
95
|
// ---------------------------------------------------------------------------
|
|
19
96
|
// JSON Schema builder for tool inputs
|
|
20
97
|
// ---------------------------------------------------------------------------
|
|
@@ -84,9 +161,12 @@ async function executeCall(endpoint, args) {
|
|
|
84
161
|
const headers = {
|
|
85
162
|
Accept: "application/json",
|
|
86
163
|
};
|
|
87
|
-
const
|
|
88
|
-
if (
|
|
89
|
-
|
|
164
|
+
const isLocal = url.startsWith("http://localhost") || url.startsWith("http://127.0.0.1");
|
|
165
|
+
if (!isLocal) {
|
|
166
|
+
const token = await getAccessToken(endpoint.scope);
|
|
167
|
+
if (token) {
|
|
168
|
+
headers.Authorization = `Bearer ${token}`;
|
|
169
|
+
}
|
|
90
170
|
}
|
|
91
171
|
let fetchBody;
|
|
92
172
|
if (args.body !== undefined && endpoint.method !== "GET") {
|
|
@@ -112,17 +192,13 @@ async function executeCall(endpoint, args) {
|
|
|
112
192
|
// Server setup
|
|
113
193
|
// ---------------------------------------------------------------------------
|
|
114
194
|
async function main() {
|
|
115
|
-
|
|
195
|
+
await loadEndpoints(currentEnvironment);
|
|
116
196
|
if (endpoints.length === 0) {
|
|
117
197
|
console.error("[snokam-mcp] No endpoints loaded. Ensure specs/*.json files exist.");
|
|
118
198
|
}
|
|
119
|
-
const endpointsByTool = new Map();
|
|
120
|
-
for (const ep of endpoints) {
|
|
121
|
-
endpointsByTool.set(ep.toolName, ep);
|
|
122
|
-
}
|
|
123
199
|
const server = new Server({
|
|
124
200
|
name: "snokam",
|
|
125
|
-
version: "0.
|
|
201
|
+
version: "0.3.0",
|
|
126
202
|
}, {
|
|
127
203
|
capabilities: {
|
|
128
204
|
tools: {},
|
|
@@ -170,7 +246,7 @@ async function main() {
|
|
|
170
246
|
|
|
171
247
|
## Environment
|
|
172
248
|
|
|
173
|
-
Currently connected to: **${
|
|
249
|
+
Currently connected to: **${currentEnvironment}**
|
|
174
250
|
|
|
175
251
|
## Available Services
|
|
176
252
|
|
|
@@ -218,15 +294,106 @@ Controls: Sonos speakers, lights, YouTube queue
|
|
|
218
294
|
});
|
|
219
295
|
// List tools
|
|
220
296
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
221
|
-
tools:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
297
|
+
tools: [
|
|
298
|
+
switchToolDef,
|
|
299
|
+
setUrlToolDef,
|
|
300
|
+
resetUrlToolDef,
|
|
301
|
+
...endpoints.map((ep) => ({
|
|
302
|
+
name: ep.toolName,
|
|
303
|
+
description: ep.description || ep.summary || `${ep.method} ${ep.path}`,
|
|
304
|
+
inputSchema: buildInputSchema(ep),
|
|
305
|
+
})),
|
|
306
|
+
],
|
|
226
307
|
}));
|
|
227
308
|
// Call tool
|
|
228
309
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
229
310
|
const { name, arguments: args = {} } = request.params;
|
|
311
|
+
// Handle SwitchEnvironment
|
|
312
|
+
if (name === SWITCH_TOOL_NAME) {
|
|
313
|
+
const env = String(args.environment ?? "");
|
|
314
|
+
if (!VALID_ENVIRONMENTS.includes(env)) {
|
|
315
|
+
return {
|
|
316
|
+
content: [
|
|
317
|
+
{
|
|
318
|
+
type: "text",
|
|
319
|
+
text: `Invalid environment: ${env}. Must be one of: ${VALID_ENVIRONMENTS.join(", ")}`,
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
isError: true,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
if (env === currentEnvironment) {
|
|
326
|
+
return {
|
|
327
|
+
content: [
|
|
328
|
+
{
|
|
329
|
+
type: "text",
|
|
330
|
+
text: `Already connected to ${env} (${endpoints.length} endpoints)`,
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
await loadEndpoints(env);
|
|
336
|
+
// Notify client that the tool list has changed
|
|
337
|
+
await server.sendToolListChanged();
|
|
338
|
+
return {
|
|
339
|
+
content: [
|
|
340
|
+
{
|
|
341
|
+
type: "text",
|
|
342
|
+
text: `Switched to ${env} environment. Loaded ${endpoints.length} endpoints.`,
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
// Handle SetServiceUrl
|
|
348
|
+
if (name === SET_URL_TOOL_NAME) {
|
|
349
|
+
const service = String(args.service ?? "");
|
|
350
|
+
const url = String(args.url ?? "");
|
|
351
|
+
const serviceEndpoints = endpoints.filter((ep) => ep.service === service);
|
|
352
|
+
if (serviceEndpoints.length === 0) {
|
|
353
|
+
const available = [
|
|
354
|
+
...new Set(endpoints.map((ep) => ep.service)),
|
|
355
|
+
].sort();
|
|
356
|
+
return {
|
|
357
|
+
content: [
|
|
358
|
+
{
|
|
359
|
+
type: "text",
|
|
360
|
+
text: `Unknown service: ${service}. Available: ${available.join(", ")}`,
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
isError: true,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
serviceUrlOverrides.set(service, url);
|
|
367
|
+
for (const ep of serviceEndpoints) {
|
|
368
|
+
ep.baseUrl = url;
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
content: [
|
|
372
|
+
{
|
|
373
|
+
type: "text",
|
|
374
|
+
text: `Overrode ${service} → ${url} (${serviceEndpoints.length} endpoints). Auth ${url.startsWith("http://localhost") || url.startsWith("http://127.0.0.1") ? "skipped" : "active"}.`,
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
// Handle ResetServiceUrl
|
|
380
|
+
if (name === RESET_URL_TOOL_NAME) {
|
|
381
|
+
const service = args.service ? String(args.service) : undefined;
|
|
382
|
+
if (service) {
|
|
383
|
+
serviceUrlOverrides.delete(service);
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
serviceUrlOverrides.clear();
|
|
387
|
+
}
|
|
388
|
+
// Reload to restore original URLs
|
|
389
|
+
await loadEndpoints(currentEnvironment);
|
|
390
|
+
const msg = service
|
|
391
|
+
? `Reset ${service} to environment default`
|
|
392
|
+
: `Reset all service URL overrides`;
|
|
393
|
+
return {
|
|
394
|
+
content: [{ type: "text", text: msg }],
|
|
395
|
+
};
|
|
396
|
+
}
|
|
230
397
|
const endpoint = endpointsByTool.get(name);
|
|
231
398
|
if (!endpoint) {
|
|
232
399
|
return {
|