anyapi-mcp-server 1.1.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.
@@ -0,0 +1,334 @@
1
+ import { GraphQLSchema, GraphQLObjectType, GraphQLInputObjectType, GraphQLString, GraphQLInt, GraphQLFloat, GraphQLBoolean, GraphQLList, GraphQLNonNull, graphql as executeGraphQL, printSchema, } from "graphql";
2
+ const DEFAULT_ARRAY_LIMIT = 50;
3
+ const MAX_SAMPLE_SIZE = 10;
4
+ // Schema cache keyed by "METHOD:/path/template"
5
+ const schemaCache = new Map();
6
+ /**
7
+ * If data is an array, slice it to [offset, offset+limit) and return metadata.
8
+ * Returns the original data unchanged if it's not an array.
9
+ */
10
+ export function truncateIfArray(data, limit, offset) {
11
+ if (!Array.isArray(data)) {
12
+ return { data, truncated: false, total: null };
13
+ }
14
+ const total = data.length;
15
+ const off = offset ?? 0;
16
+ const lim = limit ?? DEFAULT_ARRAY_LIMIT;
17
+ const sliced = data.slice(off, off + lim);
18
+ return { data: sliced, truncated: sliced.length < total, total };
19
+ }
20
+ function cacheKey(method, pathTemplate) {
21
+ return `${method}:${pathTemplate}`;
22
+ }
23
+ /**
24
+ * Sanitize a JSON key into a valid GraphQL field name.
25
+ * Dashes, dots, spaces become underscores. Leading digits get prefixed.
26
+ * Leading double underscores are stripped (GraphQL reserves __ for introspection).
27
+ */
28
+ function sanitizeFieldName(name) {
29
+ let s = name.replace(/[-. ]/g, "_");
30
+ if (/^[0-9]/.test(s)) {
31
+ s = "_" + s;
32
+ }
33
+ s = s.replace(/[^_a-zA-Z0-9]/g, "_");
34
+ s = s.replace(/^_+/, (match) => (match.length === 1 ? "_" : "f_"));
35
+ if (!s)
36
+ s = "f_empty";
37
+ return s;
38
+ }
39
+ /**
40
+ * Derive a valid GraphQL type name from method + path template.
41
+ * e.g. "GET:/api/card/{id}" → "GET_api_card_id"
42
+ */
43
+ function deriveTypeName(method, pathTemplate) {
44
+ let name = `${method}_${pathTemplate}`
45
+ .replace(/[^a-zA-Z0-9]/g, "_")
46
+ .replace(/_+/g, "_")
47
+ .replace(/^_|_$/g, "");
48
+ if (name.startsWith("__"))
49
+ name = name.slice(1);
50
+ return name || "Unknown";
51
+ }
52
+ function inferScalarType(value) {
53
+ switch (typeof value) {
54
+ case "string":
55
+ return GraphQLString;
56
+ case "number":
57
+ return Number.isInteger(value) ? GraphQLInt : GraphQLFloat;
58
+ case "boolean":
59
+ return GraphQLBoolean;
60
+ default:
61
+ return GraphQLString;
62
+ }
63
+ }
64
+ /**
65
+ * Merge multiple sample objects into a single "super-object" that contains
66
+ * every key seen across all samples. First non-null value wins for each key.
67
+ * Nested objects are merged recursively.
68
+ */
69
+ function mergeSamples(items) {
70
+ const merged = {};
71
+ for (const item of items) {
72
+ for (const [key, value] of Object.entries(item)) {
73
+ if (!(key in merged) || merged[key] === null || merged[key] === undefined) {
74
+ merged[key] = value;
75
+ }
76
+ else if (typeof value === "object" && value !== null && !Array.isArray(value) &&
77
+ typeof merged[key] === "object" && merged[key] !== null && !Array.isArray(merged[key])) {
78
+ merged[key] = mergeSamples([
79
+ merged[key],
80
+ value,
81
+ ]);
82
+ }
83
+ else if (Array.isArray(value) && Array.isArray(merged[key])) {
84
+ if (merged[key].length === 0 && value.length > 0) {
85
+ merged[key] = value;
86
+ }
87
+ }
88
+ }
89
+ }
90
+ return merged;
91
+ }
92
+ /**
93
+ * Sample and merge multiple array elements for richer type inference.
94
+ */
95
+ function mergeArraySamples(arr) {
96
+ const sampleSize = Math.min(arr.length, MAX_SAMPLE_SIZE);
97
+ const objectSamples = arr
98
+ .slice(0, sampleSize)
99
+ .filter((s) => typeof s === "object" && s !== null && !Array.isArray(s));
100
+ if (objectSamples.length === 0)
101
+ return null;
102
+ return mergeSamples(objectSamples);
103
+ }
104
+ /**
105
+ * Recursively infer a GraphQL type from a JSON value.
106
+ * For objects, creates a named GraphQLObjectType with explicit resolvers
107
+ * that map sanitized field names back to original JSON keys.
108
+ */
109
+ function inferType(value, typeName, typeRegistry) {
110
+ if (value === null || value === undefined) {
111
+ return GraphQLString;
112
+ }
113
+ if (Array.isArray(value)) {
114
+ if (value.length === 0) {
115
+ return new GraphQLList(GraphQLString);
116
+ }
117
+ // Sample multiple elements for richer type inference
118
+ const merged = mergeArraySamples(value);
119
+ if (merged) {
120
+ const elementType = inferType(merged, `${typeName}_Item`, typeRegistry);
121
+ return new GraphQLList(elementType);
122
+ }
123
+ const elementType = inferType(value[0], `${typeName}_Item`, typeRegistry);
124
+ return new GraphQLList(elementType);
125
+ }
126
+ if (typeof value === "object") {
127
+ const existing = typeRegistry.get(typeName);
128
+ if (existing)
129
+ return existing;
130
+ const obj = value;
131
+ const entries = Object.entries(obj);
132
+ if (entries.length === 0) {
133
+ const emptyType = new GraphQLObjectType({
134
+ name: typeName,
135
+ fields: { _empty: { type: GraphQLString, resolve: () => null } },
136
+ });
137
+ typeRegistry.set(typeName, emptyType);
138
+ return emptyType;
139
+ }
140
+ // Reserve the name before recursing to handle circular structures
141
+ const placeholder = new GraphQLObjectType({
142
+ name: typeName,
143
+ fields: {},
144
+ });
145
+ typeRegistry.set(typeName, placeholder);
146
+ const usedNames = new Set();
147
+ const fieldConfigs = {};
148
+ for (const [originalKey, fieldValue] of entries) {
149
+ let sanitized = sanitizeFieldName(originalKey);
150
+ if (usedNames.has(sanitized)) {
151
+ let counter = 2;
152
+ while (usedNames.has(`${sanitized}_${counter}`))
153
+ counter++;
154
+ sanitized = `${sanitized}_${counter}`;
155
+ }
156
+ usedNames.add(sanitized);
157
+ const childTypeName = `${typeName}_${sanitized}`;
158
+ const fieldType = inferType(fieldValue, childTypeName, typeRegistry);
159
+ const key = originalKey;
160
+ fieldConfigs[sanitized] = {
161
+ type: fieldType,
162
+ resolve: (source) => source[key],
163
+ };
164
+ }
165
+ const realType = new GraphQLObjectType({
166
+ name: typeName,
167
+ fields: () => fieldConfigs,
168
+ });
169
+ typeRegistry.set(typeName, realType);
170
+ return realType;
171
+ }
172
+ return inferScalarType(value);
173
+ }
174
+ /**
175
+ * Map OpenAPI type strings to GraphQL input types.
176
+ */
177
+ function mapOpenApiTypeToGraphQLInput(type) {
178
+ switch (type) {
179
+ case "string":
180
+ return GraphQLString;
181
+ case "integer":
182
+ return GraphQLInt;
183
+ case "number":
184
+ return GraphQLFloat;
185
+ case "boolean":
186
+ return GraphQLBoolean;
187
+ default:
188
+ return GraphQLString;
189
+ }
190
+ }
191
+ const WRITE_METHODS = new Set(["POST", "PUT", "DELETE", "PATCH"]);
192
+ /**
193
+ * Build a GraphQL schema from an arbitrary JSON response.
194
+ *
195
+ * - Object responses: fields promoted to root Query (query as `{ id name ... }`)
196
+ * - Array responses: wrapped as `{ items [...], _count }` (query as `{ items { id name } _count }`)
197
+ * - Scalar responses: wrapped as `{ value }`
198
+ * - Write operations with requestBodySchema: adds a Mutation type
199
+ */
200
+ export function buildSchemaFromData(data, method, pathTemplate, requestBodySchema) {
201
+ const baseName = deriveTypeName(method, pathTemplate);
202
+ const typeRegistry = new Map();
203
+ let queryType;
204
+ // Array response
205
+ if (Array.isArray(data)) {
206
+ let itemType = GraphQLString;
207
+ if (data.length > 0) {
208
+ const merged = mergeArraySamples(data);
209
+ if (merged) {
210
+ itemType = inferType(merged, `${baseName}_Item`, typeRegistry);
211
+ }
212
+ else {
213
+ itemType = inferScalarType(data[0]);
214
+ }
215
+ }
216
+ queryType = new GraphQLObjectType({
217
+ name: "Query",
218
+ fields: {
219
+ items: {
220
+ type: new GraphQLList(itemType),
221
+ resolve: (source) => source,
222
+ },
223
+ _count: {
224
+ type: GraphQLInt,
225
+ resolve: (source) => source.length,
226
+ },
227
+ },
228
+ });
229
+ }
230
+ else if (typeof data === "object" && data !== null) {
231
+ // Object response
232
+ const responseType = inferType(data, baseName, typeRegistry);
233
+ queryType = new GraphQLObjectType({
234
+ name: "Query",
235
+ fields: () => {
236
+ const originalFields = responseType.getFields();
237
+ const queryFields = {};
238
+ for (const [fieldName, fieldDef] of Object.entries(originalFields)) {
239
+ queryFields[fieldName] = {
240
+ type: fieldDef.type,
241
+ resolve: fieldDef.resolve,
242
+ };
243
+ }
244
+ return queryFields;
245
+ },
246
+ });
247
+ }
248
+ else {
249
+ // Scalar response
250
+ queryType = new GraphQLObjectType({
251
+ name: "Query",
252
+ fields: {
253
+ value: {
254
+ type: GraphQLString,
255
+ resolve: (source) => String(source),
256
+ },
257
+ },
258
+ });
259
+ }
260
+ // Build mutation type for write operations with a request body schema
261
+ if (WRITE_METHODS.has(method) && requestBodySchema) {
262
+ const mutationName = `${method.toLowerCase()}_${baseName}`;
263
+ const inputFields = {};
264
+ for (const [propName, propDef] of Object.entries(requestBodySchema.properties)) {
265
+ const sanitized = sanitizeFieldName(propName);
266
+ let type = mapOpenApiTypeToGraphQLInput(propDef.type);
267
+ if (propDef.required) {
268
+ type = new GraphQLNonNull(type);
269
+ }
270
+ inputFields[sanitized] = {
271
+ type,
272
+ ...(propDef.description ? { description: propDef.description } : {}),
273
+ };
274
+ }
275
+ const hasInputFields = Object.keys(inputFields).length > 0;
276
+ const inputType = hasInputFields
277
+ ? new GraphQLInputObjectType({
278
+ name: `${baseName}_Input`,
279
+ fields: inputFields,
280
+ })
281
+ : null;
282
+ const mutationType = new GraphQLObjectType({
283
+ name: "Mutation",
284
+ fields: {
285
+ [mutationName]: {
286
+ type: queryType,
287
+ args: inputType ? { input: { type: inputType } } : {},
288
+ resolve: (source) => source,
289
+ },
290
+ },
291
+ });
292
+ return new GraphQLSchema({ query: queryType, mutation: mutationType });
293
+ }
294
+ return new GraphQLSchema({ query: queryType });
295
+ }
296
+ /**
297
+ * Get a cached schema or build + cache a new one from the response data.
298
+ */
299
+ export function getOrBuildSchema(data, method, pathTemplate, requestBodySchema) {
300
+ const key = cacheKey(method, pathTemplate);
301
+ const cached = schemaCache.get(key);
302
+ if (cached)
303
+ return cached;
304
+ const schema = buildSchemaFromData(data, method, pathTemplate, requestBodySchema);
305
+ schemaCache.set(key, schema);
306
+ return schema;
307
+ }
308
+ /**
309
+ * Convert a GraphQL schema to SDL string for display.
310
+ */
311
+ export function schemaToSDL(schema) {
312
+ return printSchema(schema);
313
+ }
314
+ /**
315
+ * Execute a GraphQL selection query against JSON data using a schema.
316
+ * The query should be a selection set like `{ id name collection { id } }`.
317
+ * Also supports mutation syntax: `mutation { ... }`.
318
+ */
319
+ export async function executeQuery(schema, data, query) {
320
+ const trimmed = query.trim();
321
+ const fullQuery = trimmed.startsWith("query") || trimmed.startsWith("mutation") || trimmed.startsWith("{")
322
+ ? trimmed
323
+ : `{ ${trimmed} }`;
324
+ const result = await executeGraphQL({
325
+ schema,
326
+ source: fullQuery,
327
+ rootValue: data,
328
+ });
329
+ if (result.errors && result.errors.length > 0) {
330
+ const messages = result.errors.map((e) => e.message).join("; ");
331
+ throw new Error(`GraphQL query error: ${messages}`);
332
+ }
333
+ return result.data;
334
+ }
package/build/index.js ADDED
@@ -0,0 +1,263 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { loadConfig } from "./config.js";
6
+ import { ApiIndex } from "./api-index.js";
7
+ import { callApi } from "./api-client.js";
8
+ import { initLogger } from "./logger.js";
9
+ import { generateSuggestions } from "./query-suggestions.js";
10
+ import { getOrBuildSchema, executeQuery, schemaToSDL, truncateIfArray, } from "./graphql-schema.js";
11
+ const config = loadConfig();
12
+ initLogger(config.logPath ?? null);
13
+ const apiIndex = new ApiIndex(config.spec);
14
+ const server = new McpServer({
15
+ name: config.name,
16
+ version: "1.1.1",
17
+ });
18
+ // --- Tool 1: list_api ---
19
+ server.tool("list_api", `List available ${config.name} API endpoints. ` +
20
+ "Call with no arguments to see all categories. " +
21
+ "Provide 'category' to list endpoints in a tag. " +
22
+ "Provide 'search' to search across paths and descriptions. " +
23
+ "The correct query format is auto-selected based on mode. " +
24
+ "You can optionally override with a custom 'query' parameter. " +
25
+ "Results are paginated with limit (default 20) and offset.", {
26
+ category: z
27
+ .string()
28
+ .optional()
29
+ .describe("Tag/category to filter by. Omit to see all categories."),
30
+ search: z
31
+ .string()
32
+ .optional()
33
+ .describe("Search keyword across endpoint paths and descriptions"),
34
+ query: z
35
+ .string()
36
+ .optional()
37
+ .describe("Optional GraphQL selection query override. If omitted, a sensible default is used automatically:\n" +
38
+ "Categories (no args): '{ items { tag endpointCount } _count }'\n" +
39
+ "Endpoints (with category/search): '{ items { method path summary tag parameters { name in required description } } _count }'"),
40
+ limit: z
41
+ .number()
42
+ .int()
43
+ .min(1)
44
+ .optional()
45
+ .describe("Max items to return (default: 20)"),
46
+ offset: z
47
+ .number()
48
+ .int()
49
+ .min(0)
50
+ .optional()
51
+ .describe("Items to skip (default: 0)"),
52
+ }, async ({ category, search, query, limit, offset }) => {
53
+ try {
54
+ const isEndpointMode = !!(search || category);
55
+ let data;
56
+ if (search) {
57
+ data = apiIndex.searchAll(search);
58
+ }
59
+ else if (category) {
60
+ data = apiIndex.listAllByCategory(category);
61
+ }
62
+ else {
63
+ data = apiIndex.listAllCategories();
64
+ }
65
+ const defaultQuery = isEndpointMode
66
+ ? "{ items { method path summary tag parameters { name in required description } } _count }"
67
+ : "{ items { tag endpointCount } _count }";
68
+ const effectiveQuery = query ?? defaultQuery;
69
+ const schema = getOrBuildSchema(data, "LIST", category ?? search ?? "_categories");
70
+ const { data: sliced, truncated, total } = truncateIfArray(data, limit ?? 20, offset);
71
+ const queryResult = await executeQuery(schema, sliced, effectiveQuery);
72
+ if (truncated && typeof queryResult === "object" && queryResult !== null) {
73
+ queryResult._meta = {
74
+ total,
75
+ offset: offset ?? 0,
76
+ limit: limit ?? 20,
77
+ hasMore: true,
78
+ };
79
+ }
80
+ return {
81
+ content: [
82
+ { type: "text", text: JSON.stringify(queryResult, null, 2) },
83
+ ],
84
+ };
85
+ }
86
+ catch (error) {
87
+ const message = error instanceof Error ? error.message : String(error);
88
+ return {
89
+ content: [
90
+ { type: "text", text: JSON.stringify({ error: message }) },
91
+ ],
92
+ isError: true,
93
+ };
94
+ }
95
+ });
96
+ // --- Tool 2: call_api ---
97
+ server.tool("call_api", `Inspect a ${config.name} API endpoint. Makes a real request and returns ONLY the ` +
98
+ "inferred GraphQL schema (SDL) showing all available fields and their types. " +
99
+ "No response data is returned — use query_api to fetch actual data. " +
100
+ "IMPORTANT: Read the returned schema carefully. The root field names in the schema " +
101
+ "are what you must use in query_api — do NOT assume generic names like 'items'. " +
102
+ "For example, if the schema shows 'products: [Product]', query as '{ products { id name } }', not '{ items { id name } }'. " +
103
+ "Also returns accepted parameters (name, location, required) from the API spec, " +
104
+ "and suggestedQueries with ready-to-use GraphQL queries. " +
105
+ "Use list_api first to discover endpoints.", {
106
+ method: z
107
+ .enum(["GET", "POST", "PUT", "DELETE", "PATCH"])
108
+ .describe("HTTP method"),
109
+ path: z
110
+ .string()
111
+ .describe("API path template (e.g. '/api/card/{id}'). Use list_api to discover paths."),
112
+ params: z
113
+ .record(z.unknown())
114
+ .optional()
115
+ .describe("Path and query parameters as key-value pairs. " +
116
+ "Path params like {id} are interpolated; remaining become query string for GET."),
117
+ body: z
118
+ .record(z.unknown())
119
+ .optional()
120
+ .describe("Request body for POST/PUT/PATCH"),
121
+ headers: z
122
+ .record(z.string())
123
+ .optional()
124
+ .describe("Additional HTTP headers for this request (e.g. { \"Authorization\": \"Bearer <token>\" }). " +
125
+ "Overrides default --header values."),
126
+ }, async ({ method, path, params, body, headers }) => {
127
+ try {
128
+ const data = await callApi(config, method, path, params, body, headers);
129
+ const endpoint = apiIndex.getEndpoint(method, path);
130
+ const schema = getOrBuildSchema(data, method, path, endpoint?.requestBodySchema);
131
+ const sdl = schemaToSDL(schema);
132
+ const result = { graphqlSchema: sdl };
133
+ if (endpoint && endpoint.parameters.length > 0) {
134
+ result.parameters = endpoint.parameters.map((p) => ({
135
+ name: p.name,
136
+ in: p.in,
137
+ required: p.required,
138
+ ...(p.description ? { description: p.description } : {}),
139
+ }));
140
+ }
141
+ // Smart query suggestions
142
+ const suggestions = generateSuggestions(schema);
143
+ if (suggestions.length > 0) {
144
+ result.suggestedQueries = suggestions;
145
+ }
146
+ if (Array.isArray(data)) {
147
+ result.totalItems = data.length;
148
+ result.hint =
149
+ "Use query_api with field names from the schema above. " +
150
+ "For raw arrays: '{ items { ... } _count }'. " +
151
+ "For paginated APIs, pass limit/offset inside params (as query string parameters to the API), " +
152
+ "NOT as top-level tool parameters.";
153
+ }
154
+ else {
155
+ result.hint =
156
+ "Use query_api with the exact root field names from the schema above (e.g. if schema shows " +
157
+ "'products: [Product]', query as '{ products { id name } }' — do NOT use '{ items { ... } }'). " +
158
+ "For paginated APIs, pass limit/offset inside params (as query string parameters to the API), " +
159
+ "NOT as top-level tool parameters.";
160
+ }
161
+ return {
162
+ content: [
163
+ { type: "text", text: JSON.stringify(result, null, 2) },
164
+ ],
165
+ };
166
+ }
167
+ catch (error) {
168
+ const message = error instanceof Error ? error.message : String(error);
169
+ return {
170
+ content: [
171
+ { type: "text", text: JSON.stringify({ error: message }) },
172
+ ],
173
+ isError: true,
174
+ };
175
+ }
176
+ });
177
+ // --- Tool 3: query_api ---
178
+ server.tool("query_api", `Fetch data from a ${config.name} API endpoint, returning only the fields you select via GraphQL. ` +
179
+ "IMPORTANT: Always run call_api first to discover the actual schema field names. " +
180
+ "Use the exact root field names from the schema — do NOT assume generic names.\n" +
181
+ "- Raw array response ([...]): '{ items { id name } _count }'\n" +
182
+ "- Object response ({products: [...]}): '{ products { id name } }' (use actual field names from schema)\n" +
183
+ "- Write operations with mutation schema: 'mutation { post_endpoint(input: { ... }) { id name } }'\n" +
184
+ "Field names with dashes are converted to underscores (e.g. created-at → created_at). " +
185
+ "PAGINATION: To paginate the API itself, pass limit/offset inside 'params' (they become query string parameters). " +
186
+ "The top-level limit/offset parameters only slice the already-fetched response locally.", {
187
+ method: z
188
+ .enum(["GET", "POST", "PUT", "DELETE", "PATCH"])
189
+ .describe("HTTP method"),
190
+ path: z
191
+ .string()
192
+ .describe("API path template (e.g. '/api/card/{id}')"),
193
+ params: z
194
+ .record(z.unknown())
195
+ .optional()
196
+ .describe("Path and query parameters. Path params like {id} are interpolated; " +
197
+ "remaining become query string for GET. " +
198
+ "For API pagination, pass limit/offset here (e.g. { limit: 20, offset: 40 })."),
199
+ body: z
200
+ .record(z.unknown())
201
+ .optional()
202
+ .describe("Request body for POST/PUT/PATCH"),
203
+ query: z
204
+ .string()
205
+ .describe("GraphQL selection query using field names from call_api schema " +
206
+ "(e.g. '{ products { id name } }' — NOT '{ items { ... } }' unless the API returns a raw array)"),
207
+ headers: z
208
+ .record(z.string())
209
+ .optional()
210
+ .describe("Additional HTTP headers for this request (e.g. { \"Authorization\": \"Bearer <token>\" }). " +
211
+ "Overrides default --header values."),
212
+ limit: z
213
+ .number()
214
+ .int()
215
+ .min(1)
216
+ .optional()
217
+ .describe("Client-side slice: max items from already-fetched response (default: 50). For API pagination, use params instead."),
218
+ offset: z
219
+ .number()
220
+ .int()
221
+ .min(0)
222
+ .optional()
223
+ .describe("Client-side slice: items to skip in already-fetched response (default: 0). For API pagination, use params instead."),
224
+ }, async ({ method, path, params, body, query, headers, limit, offset }) => {
225
+ try {
226
+ const rawData = await callApi(config, method, path, params, body, headers);
227
+ const endpoint = apiIndex.getEndpoint(method, path);
228
+ const schema = getOrBuildSchema(rawData, method, path, endpoint?.requestBodySchema);
229
+ const { data, truncated, total } = truncateIfArray(rawData, limit, offset);
230
+ const queryResult = await executeQuery(schema, data, query);
231
+ if (truncated && typeof queryResult === "object" && queryResult !== null) {
232
+ queryResult._meta = {
233
+ total,
234
+ offset: offset ?? 0,
235
+ limit: limit ?? 50,
236
+ hasMore: true,
237
+ };
238
+ }
239
+ return {
240
+ content: [
241
+ { type: "text", text: JSON.stringify(queryResult, null, 2) },
242
+ ],
243
+ };
244
+ }
245
+ catch (error) {
246
+ const message = error instanceof Error ? error.message : String(error);
247
+ return {
248
+ content: [
249
+ { type: "text", text: JSON.stringify({ error: message }) },
250
+ ],
251
+ isError: true,
252
+ };
253
+ }
254
+ });
255
+ async function main() {
256
+ const transport = new StdioServerTransport();
257
+ await server.connect(transport);
258
+ console.error(`${config.name} MCP Server running on stdio`);
259
+ }
260
+ main().catch((error) => {
261
+ console.error("Fatal error:", error);
262
+ process.exit(1);
263
+ });
@@ -0,0 +1,51 @@
1
+ import { appendFile } from "node:fs/promises";
2
+ const SENSITIVE_HEADERS = new Set([
3
+ "authorization",
4
+ "x-api-key",
5
+ "cookie",
6
+ "set-cookie",
7
+ "proxy-authorization",
8
+ ]);
9
+ const MAX_BODY_LOG_LENGTH = 10_000;
10
+ let logFilePath = null;
11
+ export function initLogger(path) {
12
+ logFilePath = path;
13
+ }
14
+ export function isLoggingEnabled() {
15
+ return logFilePath !== null;
16
+ }
17
+ function maskHeaders(headers) {
18
+ const masked = {};
19
+ for (const [key, value] of Object.entries(headers)) {
20
+ if (SENSITIVE_HEADERS.has(key.toLowerCase())) {
21
+ masked[key] = value.length > 4 ? value.slice(0, 4) + "****" : "****";
22
+ }
23
+ else {
24
+ masked[key] = value;
25
+ }
26
+ }
27
+ return masked;
28
+ }
29
+ function truncateBody(body) {
30
+ const str = typeof body === "string" ? body : JSON.stringify(body);
31
+ if (str && str.length > MAX_BODY_LOG_LENGTH) {
32
+ return str.slice(0, MAX_BODY_LOG_LENGTH) + `... [truncated, ${str.length} total chars]`;
33
+ }
34
+ return body;
35
+ }
36
+ export async function logEntry(entry) {
37
+ if (!logFilePath)
38
+ return;
39
+ try {
40
+ const sanitized = {
41
+ ...entry,
42
+ requestHeaders: maskHeaders(entry.requestHeaders),
43
+ responseBody: truncateBody(entry.responseBody),
44
+ };
45
+ const line = JSON.stringify(sanitized) + "\n";
46
+ await appendFile(logFilePath, line, "utf-8");
47
+ }
48
+ catch {
49
+ // Logging failure should never crash the server
50
+ }
51
+ }