@snokam/mcp-api 1.10.0 → 1.11.1

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.
Files changed (41) hide show
  1. package/dist/auth.d.ts +0 -15
  2. package/dist/auth.js +15 -59
  3. package/dist/builtin-tools.d.ts +109 -0
  4. package/dist/builtin-tools.js +200 -0
  5. package/dist/execute-call.d.ts +5 -0
  6. package/dist/execute-call.js +45 -0
  7. package/dist/index.d.ts +0 -7
  8. package/dist/index.js +7 -312
  9. package/dist/openapi-loader.d.ts +1 -14
  10. package/dist/openapi-loader.js +8 -49
  11. package/dist/state.d.ts +13 -0
  12. package/dist/state.js +54 -0
  13. package/dist/tool-schema.d.ts +2 -0
  14. package/dist/tool-schema.js +42 -0
  15. package/package.json +6 -6
  16. package/specs/production/accounting.json +2629 -1754
  17. package/specs/production/blog.json +1608 -1
  18. package/specs/production/broker.json +52 -24
  19. package/specs/production/chatgpt.json +299 -17
  20. package/specs/production/employees.json +3095 -242
  21. package/specs/production/events.json +366 -52
  22. package/specs/production/notifications.json +419 -12
  23. package/specs/production/office.json +818 -836
  24. package/specs/production/recruitment.json +3635 -1
  25. package/specs/production/sales.json +5250 -417
  26. package/specs/production/sanity.json +29008 -15539
  27. package/specs/production/sync.json +116 -20
  28. package/specs/production/webshop.json +38 -18
  29. package/specs/test/accounting.json +2629 -1754
  30. package/specs/test/blog.json +1608 -1
  31. package/specs/test/broker.json +52 -24
  32. package/specs/test/chatgpt.json +299 -17
  33. package/specs/test/employees.json +3095 -242
  34. package/specs/test/events.json +366 -52
  35. package/specs/test/notifications.json +419 -12
  36. package/specs/test/office.json +818 -836
  37. package/specs/test/recruitment.json +3635 -1
  38. package/specs/test/sales.json +5250 -417
  39. package/specs/test/sanity.json +29008 -15539
  40. package/specs/test/sync.json +116 -20
  41. package/specs/test/webshop.json +38 -18
package/dist/auth.d.ts CHANGED
@@ -1,16 +1 @@
1
- /**
2
- * Token acquisition for Snokam backend APIs.
3
- *
4
- * Supports three modes (auto-detected):
5
- *
6
- * 1. **OBO (On-Behalf-Of):** When SNOKAM_USER_JWT is set, exchanges the user
7
- * JWT for a service-specific token via Azure AD OBO flow.
8
- * Requires AZURE_AD_CLIENT_ID, AZURE_AD_TENANT_ID, and either
9
- * AZURE_AD_SECRET (client secret) or managed identity.
10
- *
11
- * 2. **DefaultAzureCredential:** When no user JWT is present, falls back to
12
- * Azure CLI (local dev), managed identity (Azure), etc.
13
- *
14
- * 3. **No auth:** Endpoints without a scope get no Authorization header.
15
- */
16
1
  export declare function getAccessToken(scope: string | null): Promise<string | null>;
package/dist/auth.js CHANGED
@@ -1,68 +1,24 @@
1
- /**
2
- * Token acquisition for Snokam backend APIs.
3
- *
4
- * Supports three modes (auto-detected):
5
- *
6
- * 1. **OBO (On-Behalf-Of):** When SNOKAM_USER_JWT is set, exchanges the user
7
- * JWT for a service-specific token via Azure AD OBO flow.
8
- * Requires AZURE_AD_CLIENT_ID, AZURE_AD_TENANT_ID, and either
9
- * AZURE_AD_SECRET (client secret) or managed identity.
10
- *
11
- * 2. **DefaultAzureCredential:** When no user JWT is present, falls back to
12
- * Azure CLI (local dev), managed identity (Azure), etc.
13
- *
14
- * 3. **No auth:** Endpoints without a scope get no Authorization header.
15
- */
16
- import { DefaultAzureCredential, OnBehalfOfCredential, } from "@azure/identity";
17
- // Cache credentials per scope to avoid re-creating them
18
- const credentialCache = new Map();
19
- function getOboCredential(userJwt, clientId, tenantId) {
20
- const clientSecret = process.env.AZURE_AD_SECRET;
21
- if (clientSecret) {
22
- return new OnBehalfOfCredential({
23
- tenantId,
24
- clientId,
25
- clientSecret,
26
- userAssertionToken: userJwt,
27
- });
28
- }
29
- // Managed identity as client assertion (federated identity)
30
- const mi = new DefaultAzureCredential();
31
- return new OnBehalfOfCredential({
32
- tenantId,
33
- clientId,
34
- userAssertionToken: userJwt,
35
- getAssertion: async () => {
36
- const token = await mi.getToken("api://AzureADTokenExchange");
37
- return token.token;
38
- },
39
- });
40
- }
1
+ import { readFileSync } from "fs";
2
+ import { DefaultAzureCredential } from "@azure/identity";
3
+ let defaultCredential = null;
41
4
  export async function getAccessToken(scope) {
42
5
  if (!scope)
43
6
  return null;
44
- const userJwt = process.env.SNOKAM_USER_JWT;
45
- const clientId = process.env.AZURE_AD_CLIENT_ID ?? "";
46
- const tenantId = process.env.AZURE_AD_TENANT_ID ?? "";
47
- let credential;
48
- if (userJwt && clientId && tenantId) {
49
- // OBO mode
50
- const cacheKey = `obo:${scope}`;
51
- if (!credentialCache.has(cacheKey)) {
52
- credentialCache.set(cacheKey, getOboCredential(userJwt, clientId, tenantId));
53
- }
54
- credential = credentialCache.get(cacheKey);
55
- }
56
- else {
57
- // Default credential (Azure CLI locally, managed identity in Azure)
58
- const cacheKey = "default";
59
- if (!credentialCache.has(cacheKey)) {
60
- credentialCache.set(cacheKey, new DefaultAzureCredential());
7
+ const tokenFile = process.env.SNOKAM_ACCESS_TOKEN_FILE;
8
+ if (tokenFile) {
9
+ try {
10
+ const fromFile = readFileSync(tokenFile, "utf8").trim();
11
+ if (fromFile)
12
+ return fromFile;
61
13
  }
62
- credential = credentialCache.get(cacheKey);
14
+ catch { }
63
15
  }
16
+ const preMinted = process.env.SNOKAM_ACCESS_TOKEN;
17
+ if (preMinted)
18
+ return preMinted;
64
19
  try {
65
- const token = await credential.getToken(scope);
20
+ defaultCredential ??= new DefaultAzureCredential();
21
+ const token = await defaultCredential.getToken(scope);
66
22
  return token?.token ?? null;
67
23
  }
68
24
  catch (error) {
@@ -0,0 +1,109 @@
1
+ import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3
+ export declare const SWITCH_TOOL_NAME = "SwitchEnvironment";
4
+ export declare const VALID_ENVIRONMENTS: string[];
5
+ export declare const switchToolDef: {
6
+ name: string;
7
+ description: string;
8
+ inputSchema: {
9
+ type: "object";
10
+ properties: {
11
+ environment: {
12
+ type: string;
13
+ enum: string[];
14
+ description: string;
15
+ };
16
+ };
17
+ required: string[];
18
+ };
19
+ };
20
+ export declare const SET_URL_TOOL_NAME = "SetServiceUrl";
21
+ export declare const RESET_URL_TOOL_NAME = "ResetServiceUrl";
22
+ export declare const setUrlToolDef: {
23
+ name: string;
24
+ description: string;
25
+ inputSchema: {
26
+ type: "object";
27
+ properties: {
28
+ service: {
29
+ type: string;
30
+ description: string;
31
+ };
32
+ url: {
33
+ type: string;
34
+ description: string;
35
+ };
36
+ };
37
+ required: string[];
38
+ };
39
+ };
40
+ export declare const resetUrlToolDef: {
41
+ name: string;
42
+ description: string;
43
+ inputSchema: {
44
+ type: "object";
45
+ properties: {
46
+ service: {
47
+ type: string;
48
+ description: string;
49
+ };
50
+ };
51
+ };
52
+ };
53
+ export declare const SWITCH_DOMAIN_TOOL_NAME = "SwitchDomain";
54
+ export declare const switchDomainToolDef: {
55
+ name: string;
56
+ description: string;
57
+ inputSchema: {
58
+ type: "object";
59
+ properties: {
60
+ domain: {
61
+ type: string;
62
+ description: string;
63
+ };
64
+ };
65
+ required: string[];
66
+ };
67
+ };
68
+ export declare const BUILTIN_TOOL_DEFS: ({
69
+ name: string;
70
+ description: string;
71
+ inputSchema: {
72
+ type: "object";
73
+ properties: {
74
+ environment: {
75
+ type: string;
76
+ enum: string[];
77
+ description: string;
78
+ };
79
+ };
80
+ required: string[];
81
+ };
82
+ } | {
83
+ name: string;
84
+ description: string;
85
+ inputSchema: {
86
+ type: "object";
87
+ properties: {
88
+ service: {
89
+ type: string;
90
+ description: string;
91
+ };
92
+ };
93
+ };
94
+ } | {
95
+ name: string;
96
+ description: string;
97
+ inputSchema: {
98
+ type: "object";
99
+ properties: {
100
+ domain: {
101
+ type: string;
102
+ description: string;
103
+ };
104
+ };
105
+ required: string[];
106
+ };
107
+ })[];
108
+ export declare const BUILTIN_TOOL_NAMES: Set<string>;
109
+ export declare function handleBuiltinTool(server: Server, name: string, args: Record<string, unknown>): Promise<CallToolResult>;
@@ -0,0 +1,200 @@
1
+ import { fetchSpecFromUrl } from "./openapi-loader.js";
2
+ import { activeApiHost, apiHostOverride, currentEnvironment, endpoints, loadEndpoints, normalizeDomain, replaceServiceEndpoints, serviceUrlOverrides, setApiHostOverride, } from "./state.js";
3
+ export const SWITCH_TOOL_NAME = "SwitchEnvironment";
4
+ export const VALID_ENVIRONMENTS = ["production", "test"];
5
+ export const switchToolDef = {
6
+ name: SWITCH_TOOL_NAME,
7
+ description: "Switch the Snokam MCP server between environments (production/test). Reloads all API specs for the new environment.",
8
+ inputSchema: {
9
+ type: "object",
10
+ properties: {
11
+ environment: {
12
+ type: "string",
13
+ enum: VALID_ENVIRONMENTS,
14
+ description: "The environment to switch to",
15
+ },
16
+ },
17
+ required: ["environment"],
18
+ },
19
+ };
20
+ export const SET_URL_TOOL_NAME = "SetServiceUrl";
21
+ export const RESET_URL_TOOL_NAME = "ResetServiceUrl";
22
+ export const setUrlToolDef = {
23
+ name: SET_URL_TOOL_NAME,
24
+ 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.",
25
+ inputSchema: {
26
+ type: "object",
27
+ properties: {
28
+ service: {
29
+ type: "string",
30
+ description: "The service name (e.g. employees, notifications, events)",
31
+ },
32
+ url: {
33
+ type: "string",
34
+ description: "The base URL to use (e.g. http://localhost:7071)",
35
+ },
36
+ },
37
+ required: ["service", "url"],
38
+ },
39
+ };
40
+ export const resetUrlToolDef = {
41
+ name: RESET_URL_TOOL_NAME,
42
+ description: "Reset a service's base URL back to the environment default. Call without arguments to reset all overrides.",
43
+ inputSchema: {
44
+ type: "object",
45
+ properties: {
46
+ service: {
47
+ type: "string",
48
+ description: "The service name to reset. Omit to reset all overrides.",
49
+ },
50
+ },
51
+ },
52
+ };
53
+ export const SWITCH_DOMAIN_TOOL_NAME = "SwitchDomain";
54
+ export const switchDomainToolDef = {
55
+ name: SWITCH_DOMAIN_TOOL_NAME,
56
+ 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.",
57
+ inputSchema: {
58
+ type: "object",
59
+ properties: {
60
+ domain: {
61
+ type: "string",
62
+ description: "The business's API host (e.g. acme.snosky.no). Empty string resets to the environment default.",
63
+ },
64
+ },
65
+ required: ["domain"],
66
+ },
67
+ };
68
+ export const BUILTIN_TOOL_DEFS = [
69
+ switchToolDef,
70
+ switchDomainToolDef,
71
+ setUrlToolDef,
72
+ resetUrlToolDef,
73
+ ];
74
+ export const BUILTIN_TOOL_NAMES = new Set(BUILTIN_TOOL_DEFS.map((def) => def.name));
75
+ export async function handleBuiltinTool(server, name, args) {
76
+ if (name === SWITCH_TOOL_NAME) {
77
+ return handleSwitchEnvironment(server, args);
78
+ }
79
+ if (name === SWITCH_DOMAIN_TOOL_NAME) {
80
+ return handleSwitchDomain(server, args);
81
+ }
82
+ if (name === SET_URL_TOOL_NAME) {
83
+ return handleSetServiceUrl(server, args);
84
+ }
85
+ return handleResetServiceUrl(args);
86
+ }
87
+ async function handleSwitchEnvironment(server, args) {
88
+ const env = String(args.environment ?? "");
89
+ if (!VALID_ENVIRONMENTS.includes(env)) {
90
+ return {
91
+ content: [
92
+ {
93
+ type: "text",
94
+ text: `Invalid environment: ${env}. Must be one of: ${VALID_ENVIRONMENTS.join(", ")}`,
95
+ },
96
+ ],
97
+ isError: true,
98
+ };
99
+ }
100
+ if (env === currentEnvironment) {
101
+ return {
102
+ content: [
103
+ {
104
+ type: "text",
105
+ text: `Already connected to ${env} (${endpoints.length} endpoints)`,
106
+ },
107
+ ],
108
+ };
109
+ }
110
+ await loadEndpoints(env);
111
+ await server.sendToolListChanged();
112
+ return {
113
+ content: [
114
+ {
115
+ type: "text",
116
+ text: `Switched to ${env} environment. Loaded ${endpoints.length} endpoints.`,
117
+ },
118
+ ],
119
+ };
120
+ }
121
+ async function handleSwitchDomain(server, args) {
122
+ setApiHostOverride(normalizeDomain(String(args.domain ?? "")) || null);
123
+ await loadEndpoints(currentEnvironment);
124
+ await server.sendToolListChanged();
125
+ return {
126
+ content: [
127
+ {
128
+ type: "text",
129
+ text: apiHostOverride
130
+ ? `API calls now target ${apiHostOverride}/api/{service}.`
131
+ : `API host reset to the environment default (${activeApiHost()}).`,
132
+ },
133
+ ],
134
+ };
135
+ }
136
+ async function handleSetServiceUrl(server, args) {
137
+ const service = String(args.service ?? "");
138
+ const url = String(args.url ?? "");
139
+ const isLocal = url.startsWith("http://localhost") || url.startsWith("http://127.0.0.1");
140
+ if (isLocal) {
141
+ try {
142
+ const liveEndpoints = await fetchSpecFromUrl(service, url);
143
+ replaceServiceEndpoints(service, liveEndpoints);
144
+ serviceUrlOverrides.set(service, url);
145
+ await server.sendToolListChanged();
146
+ return {
147
+ content: [
148
+ {
149
+ type: "text",
150
+ text: `Overrode ${service} → ${url}. Loaded ${liveEndpoints.length} endpoints from local swagger.`,
151
+ },
152
+ ],
153
+ };
154
+ }
155
+ catch (error) {
156
+ console.error(`[snokam-mcp] Failed to fetch swagger from ${url}, falling back to static spec:`, error instanceof Error ? error.message : error);
157
+ }
158
+ }
159
+ const serviceEndpoints = endpoints.filter((ep) => ep.service === service);
160
+ if (serviceEndpoints.length === 0) {
161
+ const available = [...new Set(endpoints.map((ep) => ep.service))].sort();
162
+ return {
163
+ content: [
164
+ {
165
+ type: "text",
166
+ text: `Unknown service: ${service}. Available: ${available.join(", ")}`,
167
+ },
168
+ ],
169
+ isError: true,
170
+ };
171
+ }
172
+ serviceUrlOverrides.set(service, url);
173
+ for (const ep of serviceEndpoints) {
174
+ ep.baseUrl = url;
175
+ }
176
+ return {
177
+ content: [
178
+ {
179
+ type: "text",
180
+ text: `Overrode ${service} → ${url} (${serviceEndpoints.length} endpoints).`,
181
+ },
182
+ ],
183
+ };
184
+ }
185
+ async function handleResetServiceUrl(args) {
186
+ const service = args.service ? String(args.service) : undefined;
187
+ if (service) {
188
+ serviceUrlOverrides.delete(service);
189
+ }
190
+ else {
191
+ serviceUrlOverrides.clear();
192
+ }
193
+ await loadEndpoints(currentEnvironment);
194
+ const msg = service
195
+ ? `Reset ${service} to environment default`
196
+ : `Reset all service URL overrides`;
197
+ return {
198
+ content: [{ type: "text", text: msg }],
199
+ };
200
+ }
@@ -0,0 +1,5 @@
1
+ import { type ApiEndpoint } from "./openapi-loader.js";
2
+ export declare function executeCall(endpoint: ApiEndpoint, args: Record<string, unknown>): Promise<{
3
+ status: number;
4
+ body: unknown;
5
+ }>;
@@ -0,0 +1,45 @@
1
+ import { getAccessToken } from "./auth.js";
2
+ export async function executeCall(endpoint, args) {
3
+ let url = `${endpoint.baseUrl}${endpoint.path}`;
4
+ const queryParams = [];
5
+ for (const param of endpoint.parameters) {
6
+ const value = args[param.name];
7
+ if (value === undefined)
8
+ continue;
9
+ if (param.in === "path") {
10
+ url = url.replace(`{${param.name}}`, encodeURIComponent(String(value)));
11
+ }
12
+ else if (param.in === "query") {
13
+ queryParams.push(`${encodeURIComponent(param.name)}=${encodeURIComponent(String(value))}`);
14
+ }
15
+ }
16
+ if (queryParams.length > 0) {
17
+ url += `?${queryParams.join("&")}`;
18
+ }
19
+ const headers = {
20
+ Accept: "application/json",
21
+ };
22
+ const token = await getAccessToken(endpoint.scope);
23
+ if (token) {
24
+ headers.Authorization = `Bearer ${token}`;
25
+ }
26
+ let fetchBody;
27
+ if (args.body !== undefined && endpoint.method !== "GET") {
28
+ headers["Content-Type"] = "application/json";
29
+ fetchBody = JSON.stringify(args.body);
30
+ }
31
+ const response = await fetch(url, {
32
+ method: endpoint.method,
33
+ headers,
34
+ body: fetchBody,
35
+ });
36
+ let body;
37
+ const contentType = response.headers.get("content-type") ?? "";
38
+ if (contentType.includes("application/json")) {
39
+ body = await response.json();
40
+ }
41
+ else {
42
+ body = await response.text();
43
+ }
44
+ return { status: response.status, body };
45
+ }
package/dist/index.d.ts CHANGED
@@ -1,9 +1,2 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * Snokam MCP Server
4
- *
5
- * Exposes Snokam backend APIs as MCP tools by reading bundled OpenAPI specs.
6
- * Auth is handled via @azure/identity — supports Azure CLI (local),
7
- * managed identity (Azure), and OBO (when SNOKAM_USER_JWT is set).
8
- */
9
2
  export {};