@chen-rmag/core-infra 1.0.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.
Files changed (71) hide show
  1. package/README.md +46 -0
  2. package/dist/ProjectContextManager.d.ts +30 -0
  3. package/dist/ProjectContextManager.js +41 -0
  4. package/dist/directory-validator.d.ts +28 -0
  5. package/dist/directory-validator.js +90 -0
  6. package/dist/index.d.ts +14 -0
  7. package/dist/index.js +44 -0
  8. package/dist/mcp/file-mcp-manager.d.ts +13 -0
  9. package/dist/mcp/file-mcp-manager.js +45 -0
  10. package/dist/mcp/index.d.ts +20 -0
  11. package/dist/mcp/index.js +16 -0
  12. package/dist/mcp/mcp-client.d.ts +127 -0
  13. package/dist/mcp/mcp-client.js +165 -0
  14. package/dist/mcp/mcp-manager.d.ts +20 -0
  15. package/dist/mcp/mcp-manager.js +98 -0
  16. package/dist/mcp/playwright-mcp-manager.d.ts +18 -0
  17. package/dist/mcp/playwright-mcp-manager.js +115 -0
  18. package/dist/model.d.ts +10 -0
  19. package/dist/model.js +207 -0
  20. package/dist/repositories/BaseRepository.d.ts +68 -0
  21. package/dist/repositories/BaseRepository.js +212 -0
  22. package/dist/repositories/DirectoryRepository.d.ts +69 -0
  23. package/dist/repositories/DirectoryRepository.js +335 -0
  24. package/dist/repositories/ExplorationRepository.d.ts +33 -0
  25. package/dist/repositories/ExplorationRepository.js +53 -0
  26. package/dist/repositories/FileRepository.d.ts +55 -0
  27. package/dist/repositories/FileRepository.js +131 -0
  28. package/dist/repositories/ModelConfigRepository.d.ts +33 -0
  29. package/dist/repositories/ModelConfigRepository.js +51 -0
  30. package/dist/repositories/ProjectRepository.d.ts +31 -0
  31. package/dist/repositories/ProjectRepository.js +66 -0
  32. package/dist/repositories/SettingsRepository.d.ts +18 -0
  33. package/dist/repositories/SettingsRepository.js +71 -0
  34. package/dist/repositories/TableDataRepository.d.ts +21 -0
  35. package/dist/repositories/TableDataRepository.js +32 -0
  36. package/dist/repositories/TestCaseRepository.d.ts +120 -0
  37. package/dist/repositories/TestCaseRepository.js +463 -0
  38. package/dist/repositories/TestPlanRepository.d.ts +34 -0
  39. package/dist/repositories/TestPlanRepository.js +79 -0
  40. package/dist/repositories/TestResultRepository.d.ts +29 -0
  41. package/dist/repositories/TestResultRepository.js +53 -0
  42. package/dist/repositories/index.d.ts +16 -0
  43. package/dist/repositories/index.js +30 -0
  44. package/dist/storageService.d.ts +129 -0
  45. package/dist/storageService.js +297 -0
  46. package/dist/types.d.ts +217 -0
  47. package/dist/types.js +2 -0
  48. package/package.json +32 -0
  49. package/src/directory-validator.ts +98 -0
  50. package/src/index.ts +26 -0
  51. package/src/mcp/file-mcp-manager.ts +50 -0
  52. package/src/mcp/index.ts +35 -0
  53. package/src/mcp/mcp-client.ts +209 -0
  54. package/src/mcp/mcp-manager.ts +118 -0
  55. package/src/mcp/playwright-mcp-manager.ts +127 -0
  56. package/src/model.ts +234 -0
  57. package/src/repositories/BaseRepository.ts +193 -0
  58. package/src/repositories/DirectoryRepository.ts +393 -0
  59. package/src/repositories/ExplorationRepository.ts +57 -0
  60. package/src/repositories/FileRepository.ts +153 -0
  61. package/src/repositories/ModelConfigRepository.ts +55 -0
  62. package/src/repositories/ProjectRepository.ts +70 -0
  63. package/src/repositories/SettingsRepository.ts +38 -0
  64. package/src/repositories/TableDataRepository.ts +33 -0
  65. package/src/repositories/TestCaseRepository.ts +521 -0
  66. package/src/repositories/TestPlanRepository.ts +89 -0
  67. package/src/repositories/TestResultRepository.ts +56 -0
  68. package/src/repositories/index.ts +17 -0
  69. package/src/storageService.ts +404 -0
  70. package/src/types.ts +246 -0
  71. package/tsconfig.json +19 -0
@@ -0,0 +1,217 @@
1
+ export interface TestStep {
2
+ id: string;
3
+ description: string;
4
+ status?: "running" | "success" | "error";
5
+ error?: string;
6
+ isBreakpoint?: boolean;
7
+ isAI?: boolean;
8
+ reusedTestIds?: string[];
9
+ }
10
+ export interface TableData {
11
+ testId: string;
12
+ columns: string[];
13
+ rows: Record<string, string>[];
14
+ createdAt: number;
15
+ updatedAt: number;
16
+ }
17
+ export type TestCaseType = 'functional' | 'ui' | 'edge';
18
+ export type TestCasePriority = 'high' | 'medium' | 'low';
19
+ export interface TestParameter {
20
+ key: string;
21
+ value: string;
22
+ description: string;
23
+ }
24
+ export interface TestCase {
25
+ id: string;
26
+ name: string;
27
+ description: string;
28
+ url: string;
29
+ steps: TestStep[];
30
+ playwrightScript?: string;
31
+ createdAt: number;
32
+ updatedAt: number;
33
+ status: 'draft' | 'ready' | 'completed';
34
+ path: string;
35
+ creator?: 'user' | 'ai';
36
+ table?: TableData;
37
+ type?: TestCaseType;
38
+ reusable?: boolean;
39
+ dependencies?: string[];
40
+ priority?: TestCasePriority;
41
+ parameters?: TestParameter[];
42
+ }
43
+ export type LanguageCode = 'en-US' | 'zh-CN';
44
+ export interface DirectoryInfo {
45
+ id: string;
46
+ name: string;
47
+ description?: string;
48
+ createdAt: number;
49
+ updatedAt: number;
50
+ }
51
+ export interface TestDirectory {
52
+ path: string;
53
+ name: string;
54
+ info?: DirectoryInfo;
55
+ children: TestDirectory[];
56
+ tests: TestCase[];
57
+ }
58
+ export type ModelProvider = 'openai' | 'anthropic' | 'mistral' | 'ollama' | 'openrouter';
59
+ export interface ModelConfig {
60
+ id: string;
61
+ name: string;
62
+ provider: ModelProvider;
63
+ modelName: string;
64
+ apiKey?: string;
65
+ baseURL?: string;
66
+ temperature?: number;
67
+ maxTokens?: number;
68
+ timeout?: number;
69
+ isDefault?: boolean;
70
+ createdAt: number;
71
+ updatedAt: number;
72
+ }
73
+ export interface AutomationJob {
74
+ id: string;
75
+ name: string;
76
+ url: string;
77
+ status: 'idle' | 'exploring' | 'generating' | 'completed' | 'error';
78
+ progress: number;
79
+ generatedTests: TestCase[];
80
+ insights: string[];
81
+ startedAt?: number;
82
+ completedAt?: number;
83
+ error?: string;
84
+ depth?: number;
85
+ maxTests?: number;
86
+ }
87
+ export interface ExplorationRecord {
88
+ id: string;
89
+ url: string;
90
+ status: 'pending' | 'running' | 'completed' | 'error';
91
+ progress: number;
92
+ currentPhase: string;
93
+ generatedTests: number;
94
+ testIds: string[];
95
+ config: {
96
+ modelConfigId: string;
97
+ modelName: string;
98
+ maxSteps: number;
99
+ maxTests: number;
100
+ exploreDepth: number;
101
+ explorationScope?: string;
102
+ customData?: Array<{
103
+ key: string;
104
+ value: string;
105
+ }>;
106
+ language?: LanguageCode;
107
+ };
108
+ interactions: Array<{
109
+ round: number;
110
+ type: 'exploration' | 'analysis' | 'generation';
111
+ phase: string;
112
+ message: string;
113
+ timestamp: number;
114
+ }>;
115
+ artifacts?: any;
116
+ startedAt: number;
117
+ completedAt?: number;
118
+ error?: string;
119
+ }
120
+ export interface MCPServer {
121
+ name: string;
122
+ endpoint: string;
123
+ capabilities: string[];
124
+ connected: boolean;
125
+ tools?: string[];
126
+ }
127
+ export interface BrowserCapabilities {
128
+ browsers: ('chromium' | 'firefox' | 'webkit')[];
129
+ headless: boolean;
130
+ viewport?: {
131
+ width: number;
132
+ height: number;
133
+ };
134
+ deviceScaleFactor?: number;
135
+ locale?: string;
136
+ timezone?: string;
137
+ }
138
+ export interface AppSettings {
139
+ theme: 'light' | 'dark';
140
+ aiModel: ModelConfig;
141
+ defaultBrowser: 'chromium' | 'firefox' | 'webkit';
142
+ headlessMode: boolean;
143
+ autoSave: boolean;
144
+ maxParallelTests: number;
145
+ screenshots: {
146
+ enabled: boolean;
147
+ captureOnError: boolean;
148
+ captureOnStep: boolean;
149
+ };
150
+ timeouts: {
151
+ navigation: number;
152
+ action: number;
153
+ assertion: number;
154
+ };
155
+ mcpSettings?: {
156
+ endpoint: string;
157
+ useStdio: boolean;
158
+ };
159
+ }
160
+ export interface TestResult {
161
+ id: string;
162
+ testId: string;
163
+ status: 'passed' | 'failed' | 'skipped';
164
+ duration: number;
165
+ executedAt: number;
166
+ error?: string;
167
+ summary: {
168
+ total: number;
169
+ passed: number;
170
+ failed: number;
171
+ skipped: number;
172
+ };
173
+ }
174
+ export interface ToolCallInfo {
175
+ toolCall?: ToolCall;
176
+ content: string;
177
+ status?: 'success' | 'error';
178
+ stepId?: string;
179
+ }
180
+ export type ToolCall = {
181
+ name: string;
182
+ args: Record<string, any>;
183
+ id?: string;
184
+ content?: string;
185
+ };
186
+ export interface Project {
187
+ id: string;
188
+ name: string;
189
+ description: string;
190
+ createdAt: number;
191
+ updatedAt?: number;
192
+ isActive: boolean;
193
+ }
194
+ export interface TestPlanItem {
195
+ type: 'test' | 'directory';
196
+ id: string;
197
+ name: string;
198
+ path?: string;
199
+ directoryPath?: string;
200
+ }
201
+ export interface TestPlan {
202
+ id: string;
203
+ name: string;
204
+ description: string;
205
+ projectId: string;
206
+ testIds: string[];
207
+ createdAt: number;
208
+ updatedAt: number;
209
+ status: 'ready' | 'running' | 'completed' | 'failed' | 'partial' | 'stopped';
210
+ lastExecutedAt?: number;
211
+ maxConcurrency?: number;
212
+ browsers?: Array<'chromium' | 'firefox' | 'webkit'>;
213
+ scheduledAt?: number;
214
+ passedIds?: string[];
215
+ failedIds?: string[];
216
+ skippedIds?: string[];
217
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@chen-rmag/core-infra",
3
+ "version": "1.0.0",
4
+ "description": "Core infrastructure for AI testing framework",
5
+ "main": "src/index.ts",
6
+ "types": "src/index.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "build:watch": "tsc --watch",
10
+ "prepublishOnly": "npm run build"
11
+ },
12
+ "keywords": [
13
+ "testing",
14
+ "ai",
15
+ "mcp",
16
+ "infrastructure"
17
+ ],
18
+ "author": "",
19
+ "license": "MIT",
20
+ "dependencies": {
21
+ "@langchain/anthropic": "^1.3.3",
22
+ "@langchain/core": "^1.1.12",
23
+ "@langchain/mistralai": "^1.0.2",
24
+ "@langchain/openai": "^1.2.0",
25
+ "@modelcontextprotocol/sdk": "^1.25.1",
26
+ "zod": "^4.2.1"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^25",
30
+ "typescript": "^5"
31
+ }
32
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Directory name validation utility
3
+ * Validates directory names against file system restrictions
4
+ */
5
+
6
+ /**
7
+ * Windows and Linux forbidden characters in file/directory names
8
+ * Windows: < > : " | ? * and control characters
9
+ * Linux: only / and null character, but we also exclude other common problematic chars
10
+ * We use a stricter set to ensure cross-platform compatibility
11
+ */
12
+ const FORBIDDEN_CHARS = /[<>:"|?*\\\/\x00]/;
13
+ const RESERVED_NAMES = [
14
+ 'CON', 'PRN', 'AUX', 'NUL', // Standard Windows reserved names
15
+ 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
16
+ 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9',
17
+ ];
18
+
19
+ /**
20
+ * Validate a directory name
21
+ * @param name - Directory name to validate
22
+ * @returns { valid: boolean; error?: string }
23
+ */
24
+ export function validateDirectoryName(name: string): { valid: boolean; error?: string } {
25
+ if (!name || name.trim().length === 0) {
26
+ return { valid: false, error: 'Directory name cannot be empty' };
27
+ }
28
+
29
+ // Check length
30
+ if (name.length > 255) {
31
+ return { valid: false, error: 'Directory name cannot exceed 255 characters' };
32
+ }
33
+
34
+ // Check for forbidden characters
35
+ if (FORBIDDEN_CHARS.test(name)) {
36
+ return {
37
+ valid: false,
38
+ error: 'Directory name cannot contain the following characters: < > : " | ? * \\ / and control characters',
39
+ };
40
+ }
41
+
42
+ // Check for leading/trailing spaces (problematic on Windows)
43
+ if (name !== name.trim() || name.startsWith('.') || name.endsWith('.')) {
44
+ return {
45
+ valid: false,
46
+ error: 'Directory name cannot start or end with spaces or dots',
47
+ };
48
+ }
49
+
50
+ // Check for reserved names (Windows)
51
+ const upperName = name.toUpperCase();
52
+ if (RESERVED_NAMES.includes(upperName)) {
53
+ return {
54
+ valid: false,
55
+ error: `"${name}" is a system reserved name and cannot be used as a directory name`,
56
+ };
57
+ }
58
+
59
+ return { valid: true };
60
+ }
61
+
62
+ /**
63
+ * Validate a directory path (multiple levels)
64
+ * @param path - Directory path like 'folder1/folder2/folder3'
65
+ * @returns { valid: boolean; error?: string }
66
+ */
67
+ export function validateDirectoryPath(path: string): { valid: boolean; error?: string } {
68
+ if (!path) {
69
+ // Empty path is valid (root level)
70
+ return { valid: true };
71
+ }
72
+
73
+ const parts = path.split('/').filter(p => p.length > 0);
74
+
75
+ // Check max 3 levels
76
+ if (parts.length > 3) {
77
+ return { valid: false, error: 'Directory depth cannot exceed 3 levels' };
78
+ }
79
+
80
+ // Validate each part
81
+ for (const part of parts) {
82
+ const validation = validateDirectoryName(part);
83
+ if (!validation.valid) {
84
+ return validation;
85
+ }
86
+ }
87
+
88
+ return { valid: true };
89
+ }
90
+
91
+ /**
92
+ * Normalize directory name (basic cleanup)
93
+ * @param name - Raw directory name
94
+ * @returns Normalized name
95
+ */
96
+ export function normalizeDirectoryName(name: string): string {
97
+ return name.trim().replace(/\s+/g, ' ');
98
+ }
package/src/index.ts ADDED
@@ -0,0 +1,26 @@
1
+ import 'server-only';
2
+
3
+ // Types
4
+ export * from './types';
5
+
6
+ // MCP
7
+ export type { MCPTool, MCPConfig } from './mcp/mcp-client';
8
+ export { MCPClient } from './mcp/mcp-client';
9
+ export type { ToolGroup } from './mcp/index';
10
+ export { AbstractGroupedMCPManager, type GroupedMCPManager } from './mcp/index';
11
+ export { MCPManager } from './mcp/mcp-manager';
12
+ export type { PlaywrightMCPOpts } from './mcp/playwright-mcp-manager';
13
+ export { PlaywrightMCPManager } from './mcp/playwright-mcp-manager';
14
+ export { FileMCPManager } from './mcp/file-mcp-manager';
15
+
16
+ // Repositories
17
+ export * from './repositories';
18
+
19
+ // Model
20
+ export { createModel, convertToZodSchema } from './model';
21
+
22
+ // Storage Service
23
+ export { StorageService } from './storageService';
24
+
25
+ // Directory Validator
26
+ export { validateDirectoryName, validateDirectoryPath, normalizeDirectoryName } from './directory-validator';
@@ -0,0 +1,50 @@
1
+ /**
2
+ * MCP (Model Context Protocol) Client for Playwright integration
3
+ * Integrates with https://github.com/microsoft/playwright-mcp
4
+ */
5
+
6
+ import { MCPClient, MCPConfig } from './mcp-client';
7
+ import { AbstractGroupedMCPManager } from '.';
8
+
9
+ export class FileMCPManager extends AbstractGroupedMCPManager {
10
+ private client?: MCPClient;
11
+
12
+ constructor() {
13
+ const groups = [
14
+ {
15
+ name: 'file_mcp',
16
+ description: 'Read, write files on the filesystem',
17
+ loadByDefault: false,
18
+ }
19
+ ];
20
+ super(groups);
21
+ }
22
+
23
+ async start(forced: boolean = false): Promise<void> {
24
+ const client = this.createClient();
25
+ this.client = client;
26
+ await client.connect(forced);
27
+ }
28
+
29
+ getClient(): MCPClient {
30
+ if (!this.client) {
31
+ throw new Error("MCP Client is not started yet");
32
+ }
33
+ return this.client;
34
+ }
35
+
36
+ private createClient(): MCPClient {
37
+ // Build args with headless mode
38
+ const args = [
39
+ '@cotestdev/mcp-files',
40
+ ];
41
+
42
+ const mcpConfig: MCPConfig = {
43
+ cmd: 'npx',
44
+ args: args,
45
+ env: { ...process.env } as Record<string, string>,
46
+ }
47
+
48
+ return new MCPClient(mcpConfig);
49
+ }
50
+ }
@@ -0,0 +1,35 @@
1
+ import { MCPClient } from "./mcp-client";
2
+
3
+ export interface ToolGroup {
4
+ name: string;
5
+ description: string;
6
+ tools?: string[];
7
+ loadByDefault: boolean;
8
+ }
9
+
10
+ export interface GroupedMCPManager {
11
+ groups: ToolGroup[];
12
+ start(forced?: boolean): Promise<void>;
13
+ getToolGroupInfo(): Record<string, string>;
14
+ getClient(): MCPClient;
15
+ }
16
+
17
+ export abstract class AbstractGroupedMCPManager implements GroupedMCPManager {
18
+ groups: ToolGroup[];
19
+
20
+ constructor(groups: ToolGroup[]) {
21
+ this.groups = groups;
22
+ }
23
+
24
+ abstract start(forced: boolean): Promise<void>;
25
+
26
+ abstract getClient(): MCPClient
27
+
28
+ getToolGroupInfo(): Record<string, string> {
29
+ const result: Record<string, string> = {};
30
+ for (const group of this.groups) {
31
+ result[group.name] = group.description;
32
+ }
33
+ return result;
34
+ }
35
+ }
@@ -0,0 +1,209 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ /**
3
+ * MCP (Model Context Protocol) Client for Playwright integration
4
+ * Integrates with https://github.com/microsoft/playwright-mcp
5
+ */
6
+
7
+ import { ToolCall } from '../types';
8
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
9
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
10
+ import type { Tool } from '@modelcontextprotocol/sdk/types.js';
11
+
12
+ export interface MCPTool {
13
+ name: string;
14
+ description: string;
15
+ inputSchema: Record<string, unknown>;
16
+ }
17
+
18
+ export interface MCPConfig {
19
+ cmd?: string;
20
+ args: string[];
21
+ url?: string;
22
+ env?: Record<string, string>;
23
+ }
24
+
25
+ export class MCPClient {
26
+ private client: Client | null = null;
27
+ private connected = false;
28
+ private connecting = false;
29
+ private transport: StdioClientTransport | null = null;
30
+ private availableTools: MCPTool[] = [];
31
+ private config: MCPConfig;
32
+
33
+ constructor(config: MCPConfig) {
34
+ this.config = config;
35
+ }
36
+
37
+ getConfig(): MCPConfig {
38
+ return this.config;
39
+ }
40
+
41
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
42
+ async doCallTool(name: string, args: Record<string, any>, signal: AbortSignal) {
43
+ if (!this.client || !this.connected) {
44
+ throw new Error('MCP Client is not connected');
45
+ }
46
+
47
+ // Check if signal is aborted before calling
48
+ if (signal?.aborted) {
49
+ throw new Error('Operation was aborted');
50
+ }
51
+
52
+ // Ensure signal has throwIfAborted method for compatibility
53
+ if (signal && typeof signal.throwIfAborted !== 'function') {
54
+ // Polyfill for older environments
55
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
56
+ (signal as any).throwIfAborted = function () {
57
+ if (this.aborted) {
58
+ throw new Error('Operation was aborted');
59
+ }
60
+ };
61
+ }
62
+
63
+ return await this.client.callTool({ name, arguments: args }, undefined, { signal });
64
+ }
65
+
66
+ async callTool(toolCall: ToolCall, signal: AbortSignal): Promise<any> {
67
+ let toolName = toolCall.name;
68
+ let args = toolCall.args;
69
+ if (toolName === 'call_tool') {
70
+ toolName = toolCall.args.tool_name;
71
+ args = toolCall.args.args;
72
+ }
73
+
74
+ console.log('Executing tool:', toolName, 'with input:', args);
75
+
76
+ const result = await this.doCallTool(toolName, args, signal);
77
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
78
+ const isError = (result as any).isError;
79
+ if (isError) {
80
+ console.warn('Tool:', toolCall.name, ' has error:', result.content);
81
+ } else {
82
+ console.log('Tool:', toolCall.name, ' executed');
83
+ }
84
+
85
+ return result;
86
+ }
87
+
88
+ async connect(forced: boolean = false): Promise<void> {
89
+ const limit = 3;
90
+ let attempts = 0;
91
+
92
+ if (!forced && (this.connected || this.connecting)) {
93
+ console.log('MCP Client is connecting or connected');
94
+ return;
95
+ }
96
+
97
+ await this.disconnect();
98
+
99
+ this.connecting = true;
100
+
101
+ while (attempts < limit) {
102
+ try {
103
+ await this.doConnect();
104
+ break;
105
+ } catch (error) {
106
+ attempts++;
107
+ console.error(`MCP Client connection attempt ${attempts} failed:`, error);
108
+ }
109
+ }
110
+
111
+ this.connecting = false;
112
+ }
113
+
114
+ async doConnect(): Promise<void> {
115
+ // Create stdio transport - it automatically spawns the playwright-mcp process
116
+ this.transport = new StdioClientTransport({
117
+ command: this.config.cmd || 'npx',
118
+ args: this.config.args,
119
+ env: {
120
+ ...this.config.env,
121
+ } as Record<string, string>,
122
+ });
123
+
124
+ this.client = new Client(
125
+ {
126
+ name: 'test-agent-client',
127
+ version: '1.0.0',
128
+ },
129
+ {
130
+ capabilities: {},
131
+ }
132
+ );
133
+
134
+ await this.client.connect(this.transport, { timeout: 120000 });
135
+ this.connected = true;
136
+
137
+ // Load available tools
138
+ await this.loadAvailableTools();
139
+ console.log(`Connected to MCP server with ${this.availableTools.length} tools`);
140
+ }
141
+
142
+ private async loadAvailableTools(): Promise<void> {
143
+ if (!this.client) return;
144
+
145
+ const response = await this.client.listTools();
146
+ this.availableTools = response.tools.map((tool: Tool) => ({
147
+ name: tool.name,
148
+ description: tool.description || '',
149
+ inputSchema: normalizeInputSchema(tool.name, tool.inputSchema),
150
+ }));
151
+ }
152
+
153
+ getAvailableTools(): MCPTool[] {
154
+ return this.availableTools;
155
+ }
156
+
157
+ getToolByName(name: string): MCPTool | undefined {
158
+ return this.availableTools.find((t) => t.name === name);
159
+ }
160
+
161
+ getToolsDescription(): string {
162
+ return this.availableTools.map((t) => `- ${t.name}: ${t.description}`).join('\n');
163
+ }
164
+
165
+ async disconnect(): Promise<void> {
166
+ try {
167
+ if (this.client) {
168
+ await this.client.close();
169
+ }
170
+ this.connected = false;
171
+ this.availableTools = [];
172
+ this.transport = null;
173
+ this.client = null;
174
+ console.log('Disconnected from MCP server');
175
+ } catch (error) {
176
+ console.error('Error disconnecting from MCP server:', error);
177
+ }
178
+ }
179
+
180
+ isConnected(): boolean {
181
+ return this.connected;
182
+ }
183
+ }
184
+
185
+ function normalizeInputSchema(name: string, inputSchema: unknown): Record<string, unknown> {
186
+ const schema = (inputSchema ?? {}) as Record<string, unknown>;
187
+
188
+ // Ensure the schema is explicitly an object schema so MCP SDK validation passes
189
+ const properties = (schema as { properties?: Record<string, unknown> }).properties || {};
190
+ const required = Array.isArray((schema as { required?: unknown }).required)
191
+ ? ((schema as { required?: unknown }).required as string[])
192
+ : [];
193
+
194
+ if (schema.type === 'object') {
195
+ return {
196
+ type: 'object',
197
+ properties,
198
+ required,
199
+ additionalProperties: schema.additionalProperties ?? false,
200
+ } as Record<string, unknown>;
201
+ }
202
+
203
+ return {
204
+ type: 'object',
205
+ properties,
206
+ required,
207
+ additionalProperties: false,
208
+ } as Record<string, unknown>;
209
+ }