@snokam/mcp-server 0.1.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/auth.d.ts +16 -0
- package/dist/auth.js +72 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +176 -0
- package/dist/openapi-loader.d.ts +45 -0
- package/dist/openapi-loader.js +126 -0
- package/package.json +28 -0
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
export declare function getAccessToken(scope: string | null): Promise<string | null>;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
}
|
|
41
|
+
export async function getAccessToken(scope) {
|
|
42
|
+
if (!scope)
|
|
43
|
+
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());
|
|
61
|
+
}
|
|
62
|
+
credential = credentialCache.get(cacheKey);
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const token = await credential.getToken(scope);
|
|
66
|
+
return token?.token ?? null;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
console.error(`[snokam-mcp] Failed to get token for scope ${scope}:`, error instanceof Error ? error.message : error);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
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
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
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
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
10
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
12
|
+
import { fetchSpecs } from "./openapi-loader.js";
|
|
13
|
+
import { getAccessToken } from "./auth.js";
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Config
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
const ENVIRONMENT = process.env.SNOKAM_ENVIRONMENT ?? "production";
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// JSON Schema builder for tool inputs
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
function buildInputSchema(endpoint) {
|
|
22
|
+
const properties = {};
|
|
23
|
+
const required = [];
|
|
24
|
+
for (const param of endpoint.parameters) {
|
|
25
|
+
const prop = {};
|
|
26
|
+
if (param.schema?.type)
|
|
27
|
+
prop.type = param.schema.type;
|
|
28
|
+
if (param.schema?.enum)
|
|
29
|
+
prop.enum = param.schema.enum;
|
|
30
|
+
if (param.schema?.format)
|
|
31
|
+
prop.format = param.schema.format;
|
|
32
|
+
if (param.schema?.items)
|
|
33
|
+
prop.items = param.schema.items;
|
|
34
|
+
if (param.description)
|
|
35
|
+
prop.description = param.description;
|
|
36
|
+
if (!prop.type)
|
|
37
|
+
prop.type = "string";
|
|
38
|
+
properties[param.name] = prop;
|
|
39
|
+
if (param.required)
|
|
40
|
+
required.push(param.name);
|
|
41
|
+
}
|
|
42
|
+
if (endpoint.requestBody) {
|
|
43
|
+
properties.body = {
|
|
44
|
+
type: "object",
|
|
45
|
+
description: endpoint.requestBody.description ?? "Request body",
|
|
46
|
+
};
|
|
47
|
+
// Extract schema from content type if available
|
|
48
|
+
const jsonContent = endpoint.requestBody.content?.["application/json"];
|
|
49
|
+
if (jsonContent?.schema) {
|
|
50
|
+
properties.body = {
|
|
51
|
+
...properties.body,
|
|
52
|
+
...jsonContent.schema,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (endpoint.requestBody.required)
|
|
56
|
+
required.push("body");
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
type: "object",
|
|
60
|
+
properties,
|
|
61
|
+
required: required.length > 0 ? required : undefined,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// HTTP call execution
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
async function executeCall(endpoint, args) {
|
|
68
|
+
let url = `${endpoint.baseUrl}${endpoint.path}`;
|
|
69
|
+
const queryParams = [];
|
|
70
|
+
for (const param of endpoint.parameters) {
|
|
71
|
+
const value = args[param.name];
|
|
72
|
+
if (value === undefined)
|
|
73
|
+
continue;
|
|
74
|
+
if (param.in === "path") {
|
|
75
|
+
url = url.replace(`{${param.name}}`, encodeURIComponent(String(value)));
|
|
76
|
+
}
|
|
77
|
+
else if (param.in === "query") {
|
|
78
|
+
queryParams.push(`${encodeURIComponent(param.name)}=${encodeURIComponent(String(value))}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (queryParams.length > 0) {
|
|
82
|
+
url += `?${queryParams.join("&")}`;
|
|
83
|
+
}
|
|
84
|
+
const headers = {
|
|
85
|
+
Accept: "application/json",
|
|
86
|
+
};
|
|
87
|
+
const token = await getAccessToken(endpoint.scope);
|
|
88
|
+
if (token) {
|
|
89
|
+
headers.Authorization = `Bearer ${token}`;
|
|
90
|
+
}
|
|
91
|
+
let fetchBody;
|
|
92
|
+
if (args.body !== undefined && endpoint.method !== "GET") {
|
|
93
|
+
headers["Content-Type"] = "application/json";
|
|
94
|
+
fetchBody = JSON.stringify(args.body);
|
|
95
|
+
}
|
|
96
|
+
const response = await fetch(url, {
|
|
97
|
+
method: endpoint.method,
|
|
98
|
+
headers,
|
|
99
|
+
body: fetchBody,
|
|
100
|
+
});
|
|
101
|
+
let body;
|
|
102
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
103
|
+
if (contentType.includes("application/json")) {
|
|
104
|
+
body = await response.json();
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
body = await response.text();
|
|
108
|
+
}
|
|
109
|
+
return { status: response.status, body };
|
|
110
|
+
}
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Server setup
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
async function main() {
|
|
115
|
+
const endpoints = await fetchSpecs(ENVIRONMENT);
|
|
116
|
+
if (endpoints.length === 0) {
|
|
117
|
+
console.error("[snokam-mcp] No endpoints loaded. Ensure specs/*.json files exist.");
|
|
118
|
+
}
|
|
119
|
+
const endpointsByTool = new Map();
|
|
120
|
+
for (const ep of endpoints) {
|
|
121
|
+
endpointsByTool.set(ep.toolName, ep);
|
|
122
|
+
}
|
|
123
|
+
const server = new Server({ name: "snokam", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
124
|
+
// List tools
|
|
125
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
126
|
+
tools: endpoints.map((ep) => ({
|
|
127
|
+
name: ep.toolName,
|
|
128
|
+
description: ep.description || ep.summary || `${ep.method} ${ep.path}`,
|
|
129
|
+
inputSchema: buildInputSchema(ep),
|
|
130
|
+
})),
|
|
131
|
+
}));
|
|
132
|
+
// Call tool
|
|
133
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
134
|
+
const { name, arguments: args = {} } = request.params;
|
|
135
|
+
const endpoint = endpointsByTool.get(name);
|
|
136
|
+
if (!endpoint) {
|
|
137
|
+
return {
|
|
138
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
139
|
+
isError: true,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const result = await executeCall(endpoint, args);
|
|
144
|
+
const text = typeof result.body === "string"
|
|
145
|
+
? result.body
|
|
146
|
+
: JSON.stringify(result.body, null, 2);
|
|
147
|
+
return {
|
|
148
|
+
content: [
|
|
149
|
+
{
|
|
150
|
+
type: "text",
|
|
151
|
+
text: `HTTP ${result.status}\n\n${text}`,
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
isError: result.status >= 400,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
return {
|
|
159
|
+
content: [
|
|
160
|
+
{
|
|
161
|
+
type: "text",
|
|
162
|
+
text: `Request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
isError: true,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
const transport = new StdioServerTransport();
|
|
170
|
+
await server.connect(transport);
|
|
171
|
+
console.error("[snokam-mcp] Server running on stdio");
|
|
172
|
+
}
|
|
173
|
+
main().catch((error) => {
|
|
174
|
+
console.error("[snokam-mcp] Fatal error:", error);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch OpenAPI specs from live Snokam APIs and produce tool definitions.
|
|
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.
|
|
8
|
+
*/
|
|
9
|
+
export interface ApiEndpoint {
|
|
10
|
+
service: string;
|
|
11
|
+
toolName: string;
|
|
12
|
+
operationId: string;
|
|
13
|
+
method: string;
|
|
14
|
+
path: string;
|
|
15
|
+
baseUrl: string;
|
|
16
|
+
summary: string;
|
|
17
|
+
description: string;
|
|
18
|
+
parameters: OpenApiParameter[];
|
|
19
|
+
requestBody: OpenApiRequestBody | null;
|
|
20
|
+
/** OAuth2 scope in `.default` format for OBO exchange, or null for public endpoints. */
|
|
21
|
+
scope: string | null;
|
|
22
|
+
}
|
|
23
|
+
interface OpenApiParameter {
|
|
24
|
+
name: string;
|
|
25
|
+
in: "query" | "path" | "header";
|
|
26
|
+
description?: string;
|
|
27
|
+
required?: boolean;
|
|
28
|
+
schema?: {
|
|
29
|
+
type?: string;
|
|
30
|
+
format?: string;
|
|
31
|
+
enum?: string[];
|
|
32
|
+
items?: {
|
|
33
|
+
type?: string;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
interface OpenApiRequestBody {
|
|
38
|
+
description?: string;
|
|
39
|
+
required?: boolean;
|
|
40
|
+
content?: Record<string, {
|
|
41
|
+
schema?: Record<string, unknown>;
|
|
42
|
+
}>;
|
|
43
|
+
}
|
|
44
|
+
export declare function fetchSpecs(environment: string): Promise<ApiEndpoint[]>;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch OpenAPI specs from live Snokam APIs and produce tool definitions.
|
|
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.
|
|
8
|
+
*/
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Known services (functions that expose OpenAPI specs)
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
const SERVICES = [
|
|
13
|
+
"accounting",
|
|
14
|
+
"broker",
|
|
15
|
+
"calculators",
|
|
16
|
+
"chatgpt",
|
|
17
|
+
"crypto",
|
|
18
|
+
"employees",
|
|
19
|
+
"events",
|
|
20
|
+
"office",
|
|
21
|
+
"platform",
|
|
22
|
+
"power-office",
|
|
23
|
+
"sync",
|
|
24
|
+
"webshop",
|
|
25
|
+
];
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Environment-aware URL resolution
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
const PROD_DOMAIN = "api.snokam.no";
|
|
30
|
+
const TEST_DOMAIN = "api.test.snokam.no";
|
|
31
|
+
function getBaseDomain(environment) {
|
|
32
|
+
return environment === "test" ? TEST_DOMAIN : PROD_DOMAIN;
|
|
33
|
+
}
|
|
34
|
+
function getBaseUrl(service, environment) {
|
|
35
|
+
return `https://${service}.${getBaseDomain(environment)}`;
|
|
36
|
+
}
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Scope extraction
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
function extractScope(operation) {
|
|
41
|
+
for (const secReq of operation.security ?? []) {
|
|
42
|
+
for (const scopes of Object.values(secReq)) {
|
|
43
|
+
if (!Array.isArray(scopes))
|
|
44
|
+
continue;
|
|
45
|
+
for (const s of scopes) {
|
|
46
|
+
if (s.startsWith("api://")) {
|
|
47
|
+
const base = s.substring(0, s.lastIndexOf("/"));
|
|
48
|
+
return `${base}/.default`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Tool naming
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
function makeToolName(service, operationId) {
|
|
59
|
+
return `${service}__${operationId}`;
|
|
60
|
+
}
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Spec parsing
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
function parseSpec(spec, service, baseUrl) {
|
|
65
|
+
const endpoints = [];
|
|
66
|
+
const paths = spec.paths;
|
|
67
|
+
if (!paths || Object.keys(paths).length === 0)
|
|
68
|
+
return endpoints;
|
|
69
|
+
for (const [path, pathItem] of Object.entries(paths)) {
|
|
70
|
+
for (const method of ["get", "post", "put", "patch", "delete"]) {
|
|
71
|
+
const operation = pathItem[method];
|
|
72
|
+
if (!operation)
|
|
73
|
+
continue;
|
|
74
|
+
const operationId = operation.operationId ?? `${method}_${path.replace(/\//g, "_")}`;
|
|
75
|
+
const summary = operation.summary ?? "";
|
|
76
|
+
const description = operation.description ?? summary;
|
|
77
|
+
const scope = extractScope(operation);
|
|
78
|
+
endpoints.push({
|
|
79
|
+
service,
|
|
80
|
+
toolName: makeToolName(service, operationId),
|
|
81
|
+
operationId,
|
|
82
|
+
method: method.toUpperCase(),
|
|
83
|
+
path,
|
|
84
|
+
baseUrl,
|
|
85
|
+
summary,
|
|
86
|
+
description,
|
|
87
|
+
parameters: operation.parameters ?? [],
|
|
88
|
+
requestBody: operation.requestBody ?? null,
|
|
89
|
+
scope,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return endpoints;
|
|
94
|
+
}
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Public API
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
export async function fetchSpecs(environment) {
|
|
99
|
+
const endpoints = [];
|
|
100
|
+
const results = await Promise.allSettled(SERVICES.map(async (service) => {
|
|
101
|
+
const baseUrl = getBaseUrl(service, environment);
|
|
102
|
+
const swaggerUrl = `${baseUrl}/swagger.json`;
|
|
103
|
+
const response = await fetch(swaggerUrl, {
|
|
104
|
+
signal: AbortSignal.timeout(10_000),
|
|
105
|
+
});
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
throw new Error(`HTTP ${response.status}`);
|
|
108
|
+
}
|
|
109
|
+
const spec = (await response.json());
|
|
110
|
+
return { service, baseUrl, spec };
|
|
111
|
+
}));
|
|
112
|
+
let successCount = 0;
|
|
113
|
+
for (const result of results) {
|
|
114
|
+
if (result.status === "fulfilled") {
|
|
115
|
+
const { service, baseUrl, spec } = result.value;
|
|
116
|
+
endpoints.push(...parseSpec(spec, service, baseUrl));
|
|
117
|
+
successCount++;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const idx = results.indexOf(result);
|
|
121
|
+
console.error(`[snokam-mcp] Failed to fetch ${SERVICES[idx]}: ${result.reason}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
console.error(`[snokam-mcp] Loaded ${endpoints.length} endpoints from ${successCount}/${SERVICES.length} services (env=${environment})`);
|
|
125
|
+
return endpoints;
|
|
126
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@snokam/mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server exposing Snokam backend APIs as tools",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"snokam-mcp": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsc --watch",
|
|
13
|
+
"start": "node dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"specs"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@azure/identity": "^4.6.0",
|
|
21
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
22
|
+
"zod": "^3.24.4"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^22.15.0",
|
|
26
|
+
"typescript": "~5.8.3"
|
|
27
|
+
}
|
|
28
|
+
}
|