@convertcom/mcp-server 0.0.1-beta.3 → 0.0.1-beta.5

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,173 @@
1
+ import { z, ZodError } from 'zod';
2
+ import { jsonSchemaToZod } from 'json-schema-to-zod';
3
+ import axios from 'axios';
4
+ import { toolDefinitionMap } from './tools.js';
5
+ import './api-config.js';
6
+ export async function executeToolByName(toolName, toolArgs) {
7
+ const toolDefinition = toolDefinitionMap.get(toolName);
8
+ if (!toolDefinition) {
9
+ return { content: [{ type: 'text', text: 'Error: Unknown tool requested: ' + toolName }] };
10
+ }
11
+ return await executeApiTool(toolName, toolDefinition, toolArgs);
12
+ }
13
+ async function executeApiTool(toolName, definition, toolArgs) {
14
+ try {
15
+ let validatedArgs;
16
+ try {
17
+ const zodSchema = getZodSchemaFromJsonSchema(definition.inputSchema, toolName);
18
+ const argsToParse = (typeof toolArgs === 'object' && toolArgs !== null) ? toolArgs : {};
19
+ validatedArgs = zodSchema.parse(argsToParse);
20
+ }
21
+ catch (error) {
22
+ if (error instanceof ZodError) {
23
+ const validationErrorMessage = 'Invalid arguments. Common issue: Array parameters (like "include", "expand") must be passed as arrays, even for single values. Example: {"query": {"include": ["goals"]}} not {"query": {"include": "goals"}}. Details: ' + JSON.stringify(error.errors, null, 2);
24
+ return { content: [{ type: 'text', text: validationErrorMessage }] };
25
+ }
26
+ else {
27
+ const errorMessage = error instanceof Error ? error.message : String(error);
28
+ return { content: [{ type: 'text', text: 'Internal error during validation setup: ' + errorMessage }] };
29
+ }
30
+ }
31
+ let urlPath = definition.pathTemplate;
32
+ const queryParams = {};
33
+ const headers = { 'Accept': 'application/json' };
34
+ let requestBodyData = undefined;
35
+ // Apply basic authentication (Bearer token)
36
+ const bearerToken = process.env.BEARER_TOKEN || process.env.API_TOKEN;
37
+ if (bearerToken) {
38
+ headers['authorization'] = `Bearer ${bearerToken}`;
39
+ }
40
+ definition.executionParameters.forEach((param) => {
41
+ const value = validatedArgs[param.name];
42
+ if (typeof value !== 'undefined' && value !== null) {
43
+ if (param.in === 'path') {
44
+ urlPath = urlPath.replace(`{${param.name}}`, encodeURIComponent(String(value)));
45
+ }
46
+ else if (param.in === 'query') {
47
+ // Always keep arrays as arrays - we'll serialize them with bracket notation later
48
+ // Note: OpenAPI spec says explode=false but Convert API requires bracket notation (explode=true behavior)
49
+ queryParams[param.name] = value;
50
+ }
51
+ else if (param.in === 'header') {
52
+ headers[param.name.toLowerCase()] = String(value);
53
+ }
54
+ }
55
+ });
56
+ if (urlPath.includes('{')) {
57
+ throw new Error(`Failed to resolve path parameters: ${urlPath}`);
58
+ }
59
+ // Build full URL with query string directly - don't rely on axios params serialization
60
+ let fullUrl = (process.env.API_BASE_URL || 'https://api.convert.com/api/v2') + urlPath;
61
+ if (Object.keys(queryParams).length > 0) {
62
+ // Serialize query params with bracket notation for arrays
63
+ const parts = [];
64
+ for (const [key, value] of Object.entries(queryParams)) {
65
+ if (Array.isArray(value)) {
66
+ value.forEach((v) => {
67
+ parts.push(`${key}[]=${encodeURIComponent(String(v))}`);
68
+ });
69
+ }
70
+ else {
71
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
72
+ }
73
+ }
74
+ fullUrl = fullUrl + '?' + parts.join('&');
75
+ }
76
+ if (definition.requestBodyContentType && typeof validatedArgs['requestBody'] !== 'undefined') {
77
+ requestBodyData = validatedArgs['requestBody'];
78
+ headers['content-type'] = definition.requestBodyContentType;
79
+ }
80
+ const config = {
81
+ method: (definition.method || 'get').toUpperCase(),
82
+ url: fullUrl,
83
+ // Don't pass params - query string is already in URL
84
+ headers,
85
+ ...(requestBodyData !== undefined && { data: requestBodyData }),
86
+ };
87
+ const response = await axios(config);
88
+ let responseText = '';
89
+ const contentType = response.headers['content-type']?.toLowerCase() || '';
90
+ if (contentType.includes('application/json') && typeof response.data === 'object' && response.data !== null) {
91
+ try {
92
+ responseText = JSON.stringify(response.data, null, 2);
93
+ }
94
+ catch {
95
+ responseText = '[Stringify Error]';
96
+ }
97
+ }
98
+ else if (typeof response.data === 'string') {
99
+ responseText = response.data;
100
+ }
101
+ else if (response.data !== undefined && response.data !== null) {
102
+ responseText = String(response.data);
103
+ }
104
+ else {
105
+ responseText = `(Status: ${response.status} - No body content)`;
106
+ }
107
+ return { content: [{ type: 'text', text: `API Response (Status: ${response.status}):\n${responseText}` }] };
108
+ }
109
+ catch (error) {
110
+ let errorMessage;
111
+ if (axios.isAxiosError(error)) {
112
+ errorMessage = formatApiError(error);
113
+ }
114
+ else if (error instanceof Error) {
115
+ errorMessage = error.message;
116
+ }
117
+ else {
118
+ errorMessage = `Unexpected error: ${String(error)}`;
119
+ }
120
+ console.error(`Error during execution: ${errorMessage}`);
121
+ return { content: [{ type: 'text', text: errorMessage }] };
122
+ }
123
+ }
124
+ function formatApiError(error) {
125
+ let message = 'API request failed.';
126
+ if (error.response) {
127
+ message = `API Error: Status ${error.response.status} (${error.response.statusText || 'Status text not available'}). `;
128
+ const responseData = error.response.data;
129
+ const MAX_LEN = 200;
130
+ if (typeof responseData === 'string') {
131
+ message += `Response: ${responseData.substring(0, MAX_LEN)}${responseData.length > MAX_LEN ? '...' : ''}`;
132
+ }
133
+ else if (responseData) {
134
+ try {
135
+ const jsonString = JSON.stringify(responseData);
136
+ message += `Response: ${jsonString.substring(0, MAX_LEN)}${jsonString.length > MAX_LEN ? '...' : ''}`;
137
+ }
138
+ catch {
139
+ message += 'Response: [Could not serialize data]';
140
+ }
141
+ }
142
+ else {
143
+ message += 'No response body received.';
144
+ }
145
+ }
146
+ else if (error.request) {
147
+ message = 'API Network Error: No response received from server.';
148
+ if (error.code)
149
+ message += ` (Code: ${error.code})`;
150
+ }
151
+ else {
152
+ message += `API Request Setup Error: ${error.message}`;
153
+ }
154
+ return message;
155
+ }
156
+ function getZodSchemaFromJsonSchema(jsonSchema, toolName) {
157
+ if (typeof jsonSchema !== 'object' || jsonSchema === null) {
158
+ return z.object({}).passthrough();
159
+ }
160
+ try {
161
+ const zodSchemaString = jsonSchemaToZod(jsonSchema);
162
+ const zodSchema = eval(zodSchemaString);
163
+ if (typeof zodSchema?.parse !== 'function') {
164
+ throw new Error('Eval did not produce a valid Zod schema.');
165
+ }
166
+ return zodSchema;
167
+ }
168
+ catch (err) {
169
+ console.error(`Failed to generate/evaluate Zod schema for '${toolName}':`, err);
170
+ return z.object({}).passthrough();
171
+ }
172
+ }
173
+ //# sourceMappingURL=executor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"executor.js","sourceRoot":"","sources":["../src/executor.ts"],"names":[],"mappings":"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"}
package/build/index.d.ts CHANGED
@@ -10,3 +10,9 @@ import './api-config.js';
10
10
  export declare const SERVER_NAME = "convert-api";
11
11
  export declare const SERVER_VERSION = "2.0.0";
12
12
  export declare const API_BASE_URL = "https://api.convert.com/api/v2";
13
+ /**
14
+ * Formats API errors for better readability
15
+ *
16
+ * @param error Axios error
17
+ * @returns Formatted error message
18
+ */
package/build/index.js CHANGED
@@ -9,61 +9,114 @@ dotenv.config();
9
9
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
10
10
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
11
11
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
12
- import { z, ZodError } from 'zod';
13
- import { jsonSchemaToZod } from 'json-schema-to-zod';
14
- import axios from 'axios';
12
+ import { executeToolByName } from './executor.js';
13
+ import { getNamespacedTools, executeNamespacedTool, isNamespace } from './namespaced.js';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { resolve, dirname } from 'node:path';
16
+ import { KbSearchService } from './kb-search.js';
15
17
  if (process.env.NODE_ENV === 'development') {
16
18
  import('mcps-logger/console');
17
19
  }
18
20
  import './api-config.js';
19
21
  import { toolDefinitionMap } from './tools.js';
20
- function getToolsForClient() {
21
- const reportingTools = [
22
- 'getaccountslist',
23
- 'getaccountlivedata',
24
- 'getaccountactivecompletedexperienceslist',
25
- 'getprojectslist',
26
- 'getproject',
27
- 'getprojectlivedata',
28
- 'getexperienceslist',
29
- 'getexperience',
30
- 'getexperiencebykey',
31
- 'getexperiencelivedata',
32
- 'getexperienceaggregatedreport',
33
- 'getexperiencedailyreport',
34
- 'getexperiencedailytrafficallocation',
35
- 'getgoal',
36
- 'getgoalbykey',
37
- 'getlocation',
38
- 'getlocationslist',
39
- 'getaudienceslist',
40
- 'getaudience',
41
- 'getfeatureslist',
42
- 'getfeature',
43
- 'getfeaturebykey',
44
- ];
45
- const toolSet = process.env.TOOLS_FOR_CLIENT ?? 'reporting';
46
- return Array.from(toolDefinitionMap.values())
47
- .filter((def) => {
48
- if (toolSet === 'readOnly') {
49
- if (def.name.match(/^get/)) {
50
- return true;
51
- }
52
- return false;
22
+ import { DebugLogger } from './debug-log.js';
23
+ const __dirname = dirname(fileURLToPath(import.meta.url));
24
+ const knowledgeBasePath = resolve(__dirname, '..', 'data', 'convert-kb.json');
25
+ const kbSearchService = new KbSearchService(knowledgeBasePath);
26
+ const SERVER_INSTRUCTIONS = [
27
+ "You are an expert Convert Experiences assistant.",
28
+ "Always call 'searchKnowledgeBase' before using API namespaces so you understand required IDs,",
29
+ "terminology, or workflows.",
30
+ "The knowledge-base search is keyword based (MiniSearch)—focus on specific terms, such as product names,",
31
+ "URLs, or field labels, for best matches.",
32
+ "Whenever an API response returns raw IDs (goals, projects, audiences, etc.), immediately enrich those",
33
+ "IDs by calling the appropriate namespace tool so the final answer contains descriptive names and details—",
34
+ "the human should never have to resolve IDs manually. Example presenting 'Goal 10001068' is not acceptable,",
35
+ "you should call the goals namespace tool to get the name of the goal.",
36
+ "CRITICAL: When fetching resources with related data (e.g., experiences with goals), ALWAYS use BOTH 'include' AND 'expand'",
37
+ "query parameters together. 'include' alone only brings the related resource IDs into the response, but 'expand' provides",
38
+ "the full object data for those IDs. WITHOUT 'expand', you will only get useless numeric IDs instead of meaningful data.",
39
+ "For example: {\"query\": {\"include\": [\"goals\", \"variations\"], \"expand\": [\"goals\", \"variations\"]}} gives you complete goal and variation objects.",
40
+ "NEVER use 'include' without 'expand' - it will make the response useless with just IDs like 'goals: [10001068, 10001069]'.",
41
+ "Pay close attention to API response codes and any 'alerts' array in the payload; they describe what to do",
42
+ "next and must shape your follow-up actions.",
43
+ "When calling MCP tools, send arguments as strict JSON with double quotes (for example: {\"query\":",
44
+ "\"install convert on shopify\"}).",
45
+ "Combine what you learn with the available tools to craft accurate, fully explained API calls."
46
+ ].join(' ');
47
+ const debugLogger = new DebugLogger(process.env.CONVERT_DEBUG_LOG === '1' || process.env.CONVERT_DEBUG_LOG?.toLowerCase() === 'true');
48
+ function normalizeLooseJson(input) {
49
+ let output = input
50
+ .replace(/[`]/g, '"')
51
+ .replace(/[\u201C\u201D]/g, '"')
52
+ .replace(/[\u2018\u2019]/g, "'")
53
+ .replace(/(\{|\,)(\s*)([A-Za-z0-9_]+)(\s*):/g, (_, start, space, key, after) => `${start}${space}"${key}"${after}:`)
54
+ .replace(/:\s*'([^']*)'/g, ': "$1"')
55
+ .replace(/,\s*([}\]])/g, '$1');
56
+ return output;
57
+ }
58
+ function tryParseJsonLike(value) {
59
+ if (!value)
60
+ return undefined;
61
+ const attempts = [value, normalizeLooseJson(value)];
62
+ for (const candidate of attempts) {
63
+ try {
64
+ return JSON.parse(candidate);
53
65
  }
54
- else if (toolSet === 'reporting') {
55
- return reportingTools.indexOf(def.name) !== -1;
66
+ catch {
67
+ continue;
56
68
  }
57
- return true;
58
- })
59
- .map((def) => {
60
- return {
61
- name: def.name,
62
- description: def.description,
63
- inputSchema: def.inputSchema,
64
- };
65
- })
66
- .filter((tool) => tool !== null);
69
+ }
70
+ return undefined;
71
+ }
72
+ function extractQueryArgument(rawArgs) {
73
+ if (rawArgs === null || typeof rawArgs === 'undefined') {
74
+ return null;
75
+ }
76
+ if (typeof rawArgs === 'string') {
77
+ const trimmed = rawArgs.trim();
78
+ if (!trimmed) {
79
+ return null;
80
+ }
81
+ const parsed = tryParseJsonLike(trimmed);
82
+ if (parsed && typeof parsed?.query === 'string') {
83
+ return parsed.query.trim();
84
+ }
85
+ return trimmed;
86
+ }
87
+ if (typeof rawArgs === 'number' || typeof rawArgs === 'boolean') {
88
+ return String(rawArgs);
89
+ }
90
+ if (typeof rawArgs === 'object') {
91
+ const argsObj = rawArgs;
92
+ const direct = argsObj.query;
93
+ if (typeof direct === 'string') {
94
+ return direct.trim();
95
+ }
96
+ if (direct !== undefined && direct !== null) {
97
+ return String(direct);
98
+ }
99
+ }
100
+ return null;
101
+ }
102
+ const searchKnowledgeBaseTool = {
103
+ name: 'searchKnowledgeBase',
104
+ description: 'Searches the Convert experience knowledge base for concepts, terminology, and workflows before executing API actions.',
105
+ inputSchema: {
106
+ type: 'object',
107
+ properties: {
108
+ query: {
109
+ type: 'string',
110
+ description: 'Describe the topic or question you want to research (e.g., "install convert on shopify").'
111
+ }
112
+ },
113
+ required: ['query'],
114
+ additionalProperties: false
115
+ }
116
+ };
117
+ function getToolsForClient() {
118
+ const toolSet = process.env.TOOLS_FOR_CLIENT ?? 'reporting';
119
+ return [searchKnowledgeBaseTool, ...getNamespacedTools(toolSet)];
67
120
  }
68
121
  /**
69
122
  * Server configuration
@@ -74,149 +127,78 @@ export const API_BASE_URL = "https://api.convert.com/api/v2";
74
127
  /**
75
128
  * MCP Server instance
76
129
  */
77
- const server = new Server({ name: SERVER_NAME, version: SERVER_VERSION }, { capabilities: { tools: {} } });
78
- /**
79
- * Security schemes from the OpenAPI spec
80
- */
81
- const securitySchemes = {};
130
+ const server = new Server({ name: SERVER_NAME, version: SERVER_VERSION }, {
131
+ capabilities: { tools: {} },
132
+ instructions: SERVER_INSTRUCTIONS
133
+ });
82
134
  server.setRequestHandler(ListToolsRequestSchema, async () => {
83
135
  return { tools: getToolsForClient() };
84
136
  });
85
137
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
86
138
  const { name: toolName, arguments: toolArgs } = request.params;
87
- const toolDefinition = toolDefinitionMap.get(toolName);
88
- if (!toolDefinition) {
89
- console.error(`Error: Unknown tool requested: ${toolName}`);
90
- return { content: [{ type: "text", text: `Error: Unknown tool requested: ${toolName}` }] };
91
- }
92
- return await executeApiTool(toolName, toolDefinition, toolArgs ?? {}, securitySchemes);
93
- });
94
- /**
95
- * Executes an API tool with the provided arguments
96
- *
97
- * @param toolName Name of the tool to execute
98
- * @param definition Tool definition
99
- * @param toolArgs Arguments provided by the user
100
- * @param allSecuritySchemes Security schemes from the OpenAPI spec
101
- * @returns Call tool result
102
- */
103
- async function executeApiTool(toolName, definition, toolArgs, allSecuritySchemes) {
104
- try {
105
- // Validate arguments against the input schema
106
- let validatedArgs;
139
+ debugLogger.log('callToolRequest', { toolName, arguments: toolArgs });
140
+ if (toolName === 'searchKnowledgeBase') {
107
141
  try {
108
- const zodSchema = getZodSchemaFromJsonSchema(definition.inputSchema, toolName);
109
- const argsToParse = (typeof toolArgs === 'object' && toolArgs !== null) ? toolArgs : {};
110
- validatedArgs = zodSchema.parse(argsToParse);
111
- }
112
- catch (error) {
113
- if (error instanceof ZodError) {
114
- const validationErrorMessage = `Invalid arguments for tool '${toolName}': ${error.errors.map(e => `${e.path.join('.')} (${e.code}): ${e.message}`).join(', ')}`;
115
- return { content: [{ type: 'text', text: validationErrorMessage }] };
142
+ const query = extractQueryArgument(toolArgs);
143
+ if (!query) {
144
+ debugLogger.log('searchKnowledgeBase.invalidArguments', { toolArgs });
145
+ return {
146
+ isError: true,
147
+ content: [{ type: 'text', text: "Error: The 'query' argument is required for searchKnowledgeBase." }],
148
+ structuredContent: { error: "The 'query' argument is required for searchKnowledgeBase." }
149
+ };
116
150
  }
117
- else {
118
- const errorMessage = error instanceof Error ? error.message : String(error);
119
- return { content: [{ type: 'text', text: `Internal error during validation setup: ${errorMessage}` }] };
120
- }
121
- }
122
- // Prepare URL, query parameters, headers, and request body
123
- let urlPath = definition.pathTemplate;
124
- const queryParams = {};
125
- const headers = { 'Accept': 'application/json' };
126
- let requestBodyData = undefined;
127
- // Apply parameters to the URL path, query, or headers
128
- definition.executionParameters.forEach((param) => {
129
- const value = validatedArgs[param.name];
130
- if (typeof value !== 'undefined' && value !== null) {
131
- if (param.in === 'path') {
132
- urlPath = urlPath.replace(`{${param.name}}`, encodeURIComponent(String(value)));
133
- }
134
- else if (param.in === 'query') {
135
- queryParams[param.name] = value;
136
- }
137
- else if (param.in === 'header') {
138
- headers[param.name.toLowerCase()] = String(value);
139
- }
151
+ const results = kbSearchService.search(query, 5);
152
+ if (results.length === 0) {
153
+ debugLogger.log('searchKnowledgeBase.noResults', { query });
154
+ return {
155
+ content: [{ type: 'text', text: `No knowledge base entries were found for the query: "${query}".` }],
156
+ structuredContent: { query, results: [], message: `No knowledge base entries found for "${query}".` }
157
+ };
140
158
  }
141
- });
142
- // Ensure all path parameters are resolved
143
- if (urlPath.includes('{')) {
144
- throw new Error(`Failed to resolve path parameters: ${urlPath}`);
159
+ const structuredResults = results.map((result) => {
160
+ return {
161
+ title: result.chunk.title,
162
+ url: result.chunk.url,
163
+ snippet: result.snippet,
164
+ score: result.score
165
+ };
166
+ });
167
+ const textSummary = `Found ${structuredResults.length} relevant articles for "${query}":\n\n` +
168
+ structuredResults.map((result, index) => {
169
+ const scoreText = Number.isFinite(result.score) ? ` (Relevance: ${(result.score * 100).toFixed(1)}%)` : '';
170
+ return `${index + 1}. ${result.title}${scoreText}\n${result.url}\n${result.snippet}`;
171
+ }).join('\n\n---\n\n');
172
+ debugLogger.log('searchKnowledgeBase.success', { query, matchCount: results.length });
173
+ return {
174
+ content: [{ type: 'text', text: textSummary }],
175
+ structuredContent: { query, results: structuredResults }
176
+ };
145
177
  }
146
- // Construct the full URL
147
- const requestUrl = API_BASE_URL ? `${API_BASE_URL}${urlPath}` : urlPath;
148
- // Handle request body if needed
149
- if (definition.requestBodyContentType && typeof validatedArgs['requestBody'] !== 'undefined') {
150
- requestBodyData = validatedArgs['requestBody'];
151
- headers['content-type'] = definition.requestBodyContentType;
152
- }
153
- // Prepare the axios request configuration
154
- const config = {
155
- method: definition.method.toUpperCase(),
156
- url: requestUrl,
157
- params: queryParams,
158
- headers: headers,
159
- ...(requestBodyData !== undefined && { data: requestBodyData }),
160
- };
161
- // Log request info to stderr (doesn't affect MCP output)
162
- console.error(`Executing tool "${toolName}": ${config.method} ${config.url}`);
163
- // Execute the request
164
- const response = await axios(config);
165
- // Process and format the response
166
- let responseText = '';
167
- const contentType = response.headers['content-type']?.toLowerCase() || '';
168
- // Handle JSON responses
169
- if (contentType.includes('application/json') && typeof response.data === 'object' && response.data !== null) {
170
- try {
171
- responseText = JSON.stringify(response.data, null, 2);
172
- }
173
- catch (e) {
174
- responseText = "[Stringify Error]";
175
- }
176
- }
177
- // Handle string responses
178
- else if (typeof response.data === 'string') {
179
- responseText = response.data;
180
- }
181
- // Handle other response types
182
- else if (response.data !== undefined && response.data !== null) {
183
- responseText = String(response.data);
184
- }
185
- // Handle empty responses
186
- else {
187
- responseText = `(Status: ${response.status} - No body content)`;
178
+ catch (error) {
179
+ const message = error instanceof Error ? error.message : String(error);
180
+ debugLogger.log('searchKnowledgeBase.error', { toolArgs, error: message });
181
+ return {
182
+ isError: true,
183
+ content: [{ type: 'text', text: `Unexpected error during knowledge base search: ${message}` }],
184
+ structuredContent: { error: message }
185
+ };
188
186
  }
189
- // Return formatted response
190
- return {
191
- content: [
192
- {
193
- type: "text",
194
- text: `API Response (Status: ${response.status}):\n${responseText}`
195
- }
196
- ],
197
- };
198
187
  }
199
- catch (error) {
200
- // Handle errors during execution
201
- let errorMessage;
202
- // Format Axios errors specially
203
- if (axios.isAxiosError(error)) {
204
- errorMessage = formatApiError(error);
205
- }
206
- // Handle standard errors
207
- else if (error instanceof Error) {
208
- errorMessage = error.message;
209
- }
210
- // Handle unexpected error types
211
- else {
212
- errorMessage = 'Unexpected error: ' + String(error);
213
- }
214
- // Log error to stderr
215
- console.error(`Error during execution of tool '${toolName}':`, errorMessage);
216
- // Return error message to client
217
- return { content: [{ type: "text", text: errorMessage }] };
188
+ if (isNamespace(toolName)) {
189
+ const response = await executeNamespacedTool(toolName, toolArgs ?? {});
190
+ debugLogger.log('namespaceTool.success', { toolName, response });
191
+ return response;
218
192
  }
219
- }
193
+ if (toolDefinitionMap.has(toolName)) {
194
+ const response = await executeToolByName(toolName, (toolArgs ?? {}));
195
+ debugLogger.log('legacyTool.success', { toolName, response });
196
+ return response;
197
+ }
198
+ console.error(`Error: Unknown tool requested: ${toolName}`);
199
+ debugLogger.log('unknownTool', { toolName });
200
+ return { content: [{ type: 'text', text: `Error: Unknown tool requested: ${toolName}` }] };
201
+ });
220
202
  /**
221
203
  * Main function to start the server
222
204
  */
@@ -253,60 +235,5 @@ main().catch((error) => {
253
235
  * @param error Axios error
254
236
  * @returns Formatted error message
255
237
  */
256
- function formatApiError(error) {
257
- let message = 'API request failed.';
258
- if (error.response) {
259
- message = `API Error: Status ${error.response.status} (${error.response.statusText || 'Status text not available'}). `;
260
- const responseData = error.response.data;
261
- const MAX_LEN = 200;
262
- if (typeof responseData === 'string') {
263
- message += `Response: ${responseData.substring(0, MAX_LEN)}${responseData.length > MAX_LEN ? '...' : ''}`;
264
- }
265
- else if (responseData) {
266
- try {
267
- const jsonString = JSON.stringify(responseData);
268
- message += `Response: ${jsonString.substring(0, MAX_LEN)}${jsonString.length > MAX_LEN ? '...' : ''}`;
269
- }
270
- catch {
271
- message += 'Response: [Could not serialize data]';
272
- }
273
- }
274
- else {
275
- message += 'No response body received.';
276
- }
277
- }
278
- else if (error.request) {
279
- message = 'API Network Error: No response received from server.';
280
- if (error.code)
281
- message += ` (Code: ${error.code})`;
282
- }
283
- else {
284
- message += `API Request Setup Error: ${error.message}`;
285
- }
286
- return message;
287
- }
288
- /**
289
- * Converts a JSON Schema to a Zod schema for runtime validation
290
- *
291
- * @param jsonSchema JSON Schema
292
- * @param toolName Tool name for error reporting
293
- * @returns Zod schema
294
- */
295
- function getZodSchemaFromJsonSchema(jsonSchema, toolName) {
296
- if (typeof jsonSchema !== 'object' || jsonSchema === null) {
297
- return z.object({}).passthrough();
298
- }
299
- try {
300
- const zodSchemaString = jsonSchemaToZod(jsonSchema);
301
- const zodSchema = eval(zodSchemaString);
302
- if (typeof zodSchema?.parse !== 'function') {
303
- throw new Error('Eval did not produce a valid Zod schema.');
304
- }
305
- return zodSchema;
306
- }
307
- catch (err) {
308
- console.error(`Failed to generate/evaluate Zod schema for '${toolName}':`, err);
309
- return z.object({}).passthrough();
310
- }
311
- }
238
+ // Removed legacy per-tool executor from index; execution handled by executor.ts and namespaced.ts
312
239
  //# sourceMappingURL=index.js.map