mcp-rubber-duck 1.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/.dockerignore +19 -0
- package/.env.desktop.example +145 -0
- package/.env.example +45 -0
- package/.env.pi.example +106 -0
- package/.env.template +165 -0
- package/.eslintrc.json +40 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +65 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +58 -0
- package/.github/ISSUE_TEMPLATE/question.md +67 -0
- package/.github/pull_request_template.md +111 -0
- package/.github/workflows/docker-build.yml +138 -0
- package/.github/workflows/release.yml +182 -0
- package/.github/workflows/security.yml +141 -0
- package/.github/workflows/semantic-release.yml +89 -0
- package/.prettierrc +10 -0
- package/.releaserc.json +66 -0
- package/CHANGELOG.md +95 -0
- package/CONTRIBUTING.md +242 -0
- package/Dockerfile +62 -0
- package/LICENSE +21 -0
- package/README.md +803 -0
- package/audit-ci.json +8 -0
- package/config/claude_desktop.json +14 -0
- package/config/config.example.json +91 -0
- package/dist/config/config.d.ts +51 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +301 -0
- package/dist/config/config.js.map +1 -0
- package/dist/config/types.d.ts +356 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +41 -0
- package/dist/config/types.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +109 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/duck-provider-enhanced.d.ts +29 -0
- package/dist/providers/duck-provider-enhanced.d.ts.map +1 -0
- package/dist/providers/duck-provider-enhanced.js +230 -0
- package/dist/providers/duck-provider-enhanced.js.map +1 -0
- package/dist/providers/enhanced-manager.d.ts +54 -0
- package/dist/providers/enhanced-manager.d.ts.map +1 -0
- package/dist/providers/enhanced-manager.js +217 -0
- package/dist/providers/enhanced-manager.js.map +1 -0
- package/dist/providers/manager.d.ts +28 -0
- package/dist/providers/manager.d.ts.map +1 -0
- package/dist/providers/manager.js +204 -0
- package/dist/providers/manager.js.map +1 -0
- package/dist/providers/provider.d.ts +29 -0
- package/dist/providers/provider.d.ts.map +1 -0
- package/dist/providers/provider.js +179 -0
- package/dist/providers/provider.js.map +1 -0
- package/dist/providers/types.d.ts +69 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/server.d.ts +24 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +501 -0
- package/dist/server.js.map +1 -0
- package/dist/services/approval.d.ts +44 -0
- package/dist/services/approval.d.ts.map +1 -0
- package/dist/services/approval.js +159 -0
- package/dist/services/approval.js.map +1 -0
- package/dist/services/cache.d.ts +21 -0
- package/dist/services/cache.d.ts.map +1 -0
- package/dist/services/cache.js +63 -0
- package/dist/services/cache.js.map +1 -0
- package/dist/services/conversation.d.ts +24 -0
- package/dist/services/conversation.d.ts.map +1 -0
- package/dist/services/conversation.js +108 -0
- package/dist/services/conversation.js.map +1 -0
- package/dist/services/function-bridge.d.ts +41 -0
- package/dist/services/function-bridge.d.ts.map +1 -0
- package/dist/services/function-bridge.js +259 -0
- package/dist/services/function-bridge.js.map +1 -0
- package/dist/services/health.d.ts +17 -0
- package/dist/services/health.d.ts.map +1 -0
- package/dist/services/health.js +77 -0
- package/dist/services/health.js.map +1 -0
- package/dist/services/mcp-client-manager.d.ts +49 -0
- package/dist/services/mcp-client-manager.d.ts.map +1 -0
- package/dist/services/mcp-client-manager.js +279 -0
- package/dist/services/mcp-client-manager.js.map +1 -0
- package/dist/tools/approve-mcp-request.d.ts +9 -0
- package/dist/tools/approve-mcp-request.d.ts.map +1 -0
- package/dist/tools/approve-mcp-request.js +111 -0
- package/dist/tools/approve-mcp-request.js.map +1 -0
- package/dist/tools/ask-duck.d.ts +9 -0
- package/dist/tools/ask-duck.d.ts.map +1 -0
- package/dist/tools/ask-duck.js +43 -0
- package/dist/tools/ask-duck.js.map +1 -0
- package/dist/tools/chat-duck.d.ts +9 -0
- package/dist/tools/chat-duck.d.ts.map +1 -0
- package/dist/tools/chat-duck.js +57 -0
- package/dist/tools/chat-duck.js.map +1 -0
- package/dist/tools/clear-conversations.d.ts +8 -0
- package/dist/tools/clear-conversations.d.ts.map +1 -0
- package/dist/tools/clear-conversations.js +17 -0
- package/dist/tools/clear-conversations.js.map +1 -0
- package/dist/tools/compare-ducks.d.ts +8 -0
- package/dist/tools/compare-ducks.d.ts.map +1 -0
- package/dist/tools/compare-ducks.js +49 -0
- package/dist/tools/compare-ducks.js.map +1 -0
- package/dist/tools/duck-council.d.ts +8 -0
- package/dist/tools/duck-council.d.ts.map +1 -0
- package/dist/tools/duck-council.js +69 -0
- package/dist/tools/duck-council.js.map +1 -0
- package/dist/tools/get-pending-approvals.d.ts +15 -0
- package/dist/tools/get-pending-approvals.d.ts.map +1 -0
- package/dist/tools/get-pending-approvals.js +74 -0
- package/dist/tools/get-pending-approvals.js.map +1 -0
- package/dist/tools/list-ducks.d.ts +9 -0
- package/dist/tools/list-ducks.d.ts.map +1 -0
- package/dist/tools/list-ducks.js +47 -0
- package/dist/tools/list-ducks.js.map +1 -0
- package/dist/tools/list-models.d.ts +8 -0
- package/dist/tools/list-models.d.ts.map +1 -0
- package/dist/tools/list-models.js +72 -0
- package/dist/tools/list-models.js.map +1 -0
- package/dist/tools/mcp-status.d.ts +17 -0
- package/dist/tools/mcp-status.d.ts.map +1 -0
- package/dist/tools/mcp-status.js +100 -0
- package/dist/tools/mcp-status.js.map +1 -0
- package/dist/utils/ascii-art.d.ts +19 -0
- package/dist/utils/ascii-art.d.ts.map +1 -0
- package/dist/utils/ascii-art.js +73 -0
- package/dist/utils/ascii-art.js.map +1 -0
- package/dist/utils/logger.d.ts +3 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +86 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/safe-logger.d.ts +23 -0
- package/dist/utils/safe-logger.d.ts.map +1 -0
- package/dist/utils/safe-logger.js +145 -0
- package/dist/utils/safe-logger.js.map +1 -0
- package/docker-compose.yml +161 -0
- package/jest.config.js +26 -0
- package/package.json +65 -0
- package/scripts/build-multiarch.sh +290 -0
- package/scripts/deploy-raspbian.sh +410 -0
- package/scripts/deploy.sh +322 -0
- package/scripts/gh-deploy.sh +343 -0
- package/scripts/setup-docker-raspbian.sh +530 -0
- package/server.json +8 -0
- package/src/config/config.ts +357 -0
- package/src/config/types.ts +89 -0
- package/src/index.ts +114 -0
- package/src/providers/duck-provider-enhanced.ts +294 -0
- package/src/providers/enhanced-manager.ts +290 -0
- package/src/providers/manager.ts +257 -0
- package/src/providers/provider.ts +207 -0
- package/src/providers/types.ts +78 -0
- package/src/server.ts +603 -0
- package/src/services/approval.ts +225 -0
- package/src/services/cache.ts +79 -0
- package/src/services/conversation.ts +146 -0
- package/src/services/function-bridge.ts +329 -0
- package/src/services/health.ts +107 -0
- package/src/services/mcp-client-manager.ts +362 -0
- package/src/tools/approve-mcp-request.ts +126 -0
- package/src/tools/ask-duck.ts +74 -0
- package/src/tools/chat-duck.ts +82 -0
- package/src/tools/clear-conversations.ts +24 -0
- package/src/tools/compare-ducks.ts +67 -0
- package/src/tools/duck-council.ts +88 -0
- package/src/tools/get-pending-approvals.ts +90 -0
- package/src/tools/list-ducks.ts +65 -0
- package/src/tools/list-models.ts +101 -0
- package/src/tools/mcp-status.ts +117 -0
- package/src/utils/ascii-art.ts +85 -0
- package/src/utils/logger.ts +116 -0
- package/src/utils/safe-logger.ts +165 -0
- package/systemd/mcp-rubber-duck-with-ollama.service +55 -0
- package/systemd/mcp-rubber-duck.service +58 -0
- package/test-functionality.js +147 -0
- package/test-mcp-interface.js +221 -0
- package/tests/ascii-art.test.ts +36 -0
- package/tests/config.test.ts +239 -0
- package/tests/conversation.test.ts +308 -0
- package/tests/mcp-bridge.test.ts +291 -0
- package/tests/providers.test.ts +269 -0
- package/tests/tools/clear-conversations.test.ts +163 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { MCPClientManager, MCPTool } from './mcp-client-manager.js';
|
|
2
|
+
import { ApprovalService } from './approval.js';
|
|
3
|
+
import { logger } from '../utils/logger.js';
|
|
4
|
+
import Ajv, { ValidateFunction } from 'ajv';
|
|
5
|
+
|
|
6
|
+
export interface FunctionDefinition {
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
parameters: Record<string, unknown>; // JSON Schema
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface FunctionCallResult {
|
|
13
|
+
success: boolean;
|
|
14
|
+
needsApproval?: boolean;
|
|
15
|
+
approvalId?: string;
|
|
16
|
+
data?: unknown;
|
|
17
|
+
error?: string;
|
|
18
|
+
message?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class FunctionBridge {
|
|
22
|
+
private mcpManager: MCPClientManager;
|
|
23
|
+
private approvalService: ApprovalService;
|
|
24
|
+
private trustedTools: Set<string> = new Set();
|
|
25
|
+
private trustedToolsByServer: Map<string, Set<string>> = new Map();
|
|
26
|
+
private ajv: unknown;
|
|
27
|
+
private toolSchemas: Map<string, Record<string, unknown>> = new Map();
|
|
28
|
+
private approvalMode: 'always' | 'trusted' | 'never';
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
mcpManager: MCPClientManager,
|
|
32
|
+
approvalService: ApprovalService,
|
|
33
|
+
trustedTools: string[] = [],
|
|
34
|
+
approvalMode: 'always' | 'trusted' | 'never' = 'always',
|
|
35
|
+
trustedToolsByServer: Record<string, string[]> = {}
|
|
36
|
+
) {
|
|
37
|
+
this.mcpManager = mcpManager;
|
|
38
|
+
this.approvalService = approvalService;
|
|
39
|
+
this.trustedTools = new Set(trustedTools);
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
|
|
41
|
+
this.ajv = new (Ajv as unknown as new (options: unknown) => unknown)({ allErrors: true, removeAdditional: 'all' });
|
|
42
|
+
this.approvalMode = approvalMode;
|
|
43
|
+
|
|
44
|
+
// Initialize per-server trusted tools
|
|
45
|
+
Object.entries(trustedToolsByServer).forEach(([serverName, tools]) => {
|
|
46
|
+
this.trustedToolsByServer.set(serverName, new Set(tools));
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async getFunctionDefinitions(): Promise<FunctionDefinition[]> {
|
|
51
|
+
try {
|
|
52
|
+
const mcpTools = await this.mcpManager.listAllTools();
|
|
53
|
+
|
|
54
|
+
const functionDefinitions: FunctionDefinition[] = mcpTools.map(tool => {
|
|
55
|
+
const functionDef = this.convertMCPToolToFunction(tool);
|
|
56
|
+
// Cache the tool schema for validation
|
|
57
|
+
const toolKey = `${tool.serverName}:${tool.name}`;
|
|
58
|
+
this.toolSchemas.set(toolKey, tool.inputSchema);
|
|
59
|
+
return functionDef;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
logger.debug(`Generated ${functionDefinitions.length} function definitions from MCP tools`);
|
|
63
|
+
return functionDefinitions;
|
|
64
|
+
|
|
65
|
+
} catch (error: unknown) {
|
|
66
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
67
|
+
logger.error('Failed to generate function definitions:', errorMessage);
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private convertMCPToolToFunction(mcpTool: MCPTool): FunctionDefinition {
|
|
73
|
+
return {
|
|
74
|
+
name: `mcp__${mcpTool.serverName}__${mcpTool.name}`,
|
|
75
|
+
description: `[${mcpTool.serverName}] ${mcpTool.description}`,
|
|
76
|
+
parameters: {
|
|
77
|
+
type: 'object',
|
|
78
|
+
properties: {
|
|
79
|
+
// Include the MCP tool's original parameters
|
|
80
|
+
...((mcpTool.inputSchema as { properties?: Record<string, unknown> })?.properties || {}),
|
|
81
|
+
|
|
82
|
+
// Add our internal parameters
|
|
83
|
+
_mcp_server: {
|
|
84
|
+
type: 'string',
|
|
85
|
+
description: 'Internal: MCP server name',
|
|
86
|
+
default: mcpTool.serverName,
|
|
87
|
+
},
|
|
88
|
+
_mcp_tool: {
|
|
89
|
+
type: 'string',
|
|
90
|
+
description: 'Internal: MCP tool name',
|
|
91
|
+
default: mcpTool.name,
|
|
92
|
+
},
|
|
93
|
+
_approval_id: {
|
|
94
|
+
type: 'string',
|
|
95
|
+
description: 'Internal: Approval ID if pre-approved',
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
required: ((mcpTool.inputSchema as { required?: string[] })?.required || []),
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private validateToolArguments(toolKey: string, args: Record<string, unknown>): { valid: boolean; errors?: string[] } {
|
|
104
|
+
const schema = this.toolSchemas.get(toolKey);
|
|
105
|
+
if (!schema) {
|
|
106
|
+
return { valid: true }; // No schema available, skip validation
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
111
|
+
const validate: ValidateFunction = (this.ajv as { compile: (schema: unknown) => ValidateFunction }).compile(schema);
|
|
112
|
+
const valid = validate(args);
|
|
113
|
+
|
|
114
|
+
if (!valid && validate.errors) {
|
|
115
|
+
const errors = validate.errors.map(err =>
|
|
116
|
+
`${err.instancePath || 'root'}: ${err.message || 'validation error'}`
|
|
117
|
+
);
|
|
118
|
+
return { valid: false, errors };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { valid: true };
|
|
122
|
+
} catch (error: unknown) {
|
|
123
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
124
|
+
logger.warn(`Failed to validate schema for ${toolKey}:`, errorMessage);
|
|
125
|
+
return { valid: true }; // Skip validation on error
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async handleFunctionCall(
|
|
130
|
+
duckName: string,
|
|
131
|
+
functionName: string,
|
|
132
|
+
args: Record<string, unknown>
|
|
133
|
+
): Promise<FunctionCallResult> {
|
|
134
|
+
try {
|
|
135
|
+
logger.info(`FunctionBridge.handleFunctionCall called: ${duckName} -> ${functionName}`);
|
|
136
|
+
logger.debug(`Approval mode: ${this.approvalMode}, Function: ${functionName}`);
|
|
137
|
+
|
|
138
|
+
// Validate that this is an MCP function
|
|
139
|
+
if (!functionName.startsWith('mcp__')) {
|
|
140
|
+
logger.warn(`Invalid function name format: ${functionName}`);
|
|
141
|
+
return {
|
|
142
|
+
success: false,
|
|
143
|
+
error: `Invalid function name: ${functionName}`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Extract MCP server and tool names from args or function name
|
|
148
|
+
const mcpServer = (args._mcp_server as string) || this.extractServerFromFunctionName(functionName);
|
|
149
|
+
const mcpTool = (args._mcp_tool as string) || this.extractToolFromFunctionName(functionName);
|
|
150
|
+
const approvalId = args._approval_id as string;
|
|
151
|
+
|
|
152
|
+
if (!mcpServer || !mcpTool) {
|
|
153
|
+
return {
|
|
154
|
+
success: false,
|
|
155
|
+
error: `Could not determine MCP server/tool from function: ${functionName}`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Clean up internal parameters from args
|
|
160
|
+
const cleanArgs = { ...args };
|
|
161
|
+
delete cleanArgs._mcp_server;
|
|
162
|
+
delete cleanArgs._mcp_tool;
|
|
163
|
+
delete cleanArgs._approval_id;
|
|
164
|
+
|
|
165
|
+
// Validate arguments against tool schema
|
|
166
|
+
const toolKey = `${mcpServer}:${mcpTool}`;
|
|
167
|
+
const validation = this.validateToolArguments(toolKey, cleanArgs);
|
|
168
|
+
if (!validation.valid) {
|
|
169
|
+
return {
|
|
170
|
+
success: false,
|
|
171
|
+
error: `Invalid arguments for ${toolKey}: ${validation.errors?.join(', ')}`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Check if approval is needed based on approval mode
|
|
176
|
+
let isTrusted = false;
|
|
177
|
+
|
|
178
|
+
// First check server-specific trusted tools
|
|
179
|
+
const serverTrustedTools = this.trustedToolsByServer.get(mcpServer);
|
|
180
|
+
if (serverTrustedTools) {
|
|
181
|
+
isTrusted = serverTrustedTools.has('*') || // Wildcard for all tools from server
|
|
182
|
+
serverTrustedTools.has(mcpTool) || // Tool name only
|
|
183
|
+
serverTrustedTools.has(toolKey); // Full server:tool format
|
|
184
|
+
logger.debug(`Server-specific trust check for ${mcpServer}: ${Array.from(serverTrustedTools).join(', ')} - isTrusted: ${isTrusted}`);
|
|
185
|
+
} else {
|
|
186
|
+
// Fall back to global trusted tools
|
|
187
|
+
isTrusted = this.trustedTools.has(toolKey) || this.trustedTools.has(mcpTool);
|
|
188
|
+
logger.debug(`Global trust check - isTrusted: ${isTrusted}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const isAlreadyApprovedForSession = this.approvalService.isToolApprovedForSession(duckName, mcpServer, mcpTool);
|
|
192
|
+
let needsApproval = false;
|
|
193
|
+
|
|
194
|
+
logger.debug(`Approval check - Mode: ${this.approvalMode}, Trusted: ${isTrusted}, SessionApproved: ${isAlreadyApprovedForSession}, ToolKey: ${toolKey}, Server: ${mcpServer}, ApprovalId: ${approvalId}`);
|
|
195
|
+
|
|
196
|
+
if (this.approvalMode === 'always') {
|
|
197
|
+
// Always require approval unless already approved or approved for session
|
|
198
|
+
needsApproval = !approvalId && !isAlreadyApprovedForSession;
|
|
199
|
+
logger.debug(`Always mode: needsApproval = ${needsApproval}`);
|
|
200
|
+
} else if (this.approvalMode === 'trusted') {
|
|
201
|
+
// Only untrusted tools need approval, unless already approved for session
|
|
202
|
+
needsApproval = !isTrusted && !approvalId && !isAlreadyApprovedForSession;
|
|
203
|
+
logger.debug(`Trusted mode: needsApproval = ${needsApproval}`);
|
|
204
|
+
} else if (this.approvalMode === 'never') {
|
|
205
|
+
// Never require approval
|
|
206
|
+
needsApproval = false;
|
|
207
|
+
logger.debug(`Never mode: needsApproval = ${needsApproval}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (needsApproval) {
|
|
211
|
+
// Create approval request
|
|
212
|
+
const request = this.approvalService.createApprovalRequest(
|
|
213
|
+
duckName,
|
|
214
|
+
mcpServer,
|
|
215
|
+
mcpTool,
|
|
216
|
+
cleanArgs
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
success: false,
|
|
221
|
+
needsApproval: true,
|
|
222
|
+
approvalId: request.id,
|
|
223
|
+
message: `🔒 Approval needed for ${duckName} to call ${mcpServer}:${mcpTool}. Request ID: ${request.id}`,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// If approval ID provided, verify it (except in 'never' mode)
|
|
228
|
+
if (approvalId && this.approvalMode !== 'never') {
|
|
229
|
+
const approvalStatus = this.approvalService.getApprovalStatus(approvalId);
|
|
230
|
+
|
|
231
|
+
if (approvalStatus !== 'approved') {
|
|
232
|
+
return {
|
|
233
|
+
success: false,
|
|
234
|
+
error: `Request not approved or expired (status: ${approvalStatus})`,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Execute the MCP tool
|
|
240
|
+
logger.info(`Executing MCP tool ${mcpServer}:${mcpTool} for ${duckName}`);
|
|
241
|
+
const result = await this.mcpManager.callTool(mcpServer, mcpTool, cleanArgs);
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
success: true,
|
|
245
|
+
data: result,
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
} catch (error: unknown) {
|
|
249
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
250
|
+
logger.error(`Function call failed for ${functionName}:`, errorMessage);
|
|
251
|
+
return {
|
|
252
|
+
success: false,
|
|
253
|
+
error: `MCP tool execution failed: ${errorMessage}`,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private extractServerFromFunctionName(functionName: string): string | null {
|
|
259
|
+
// Function name format: mcp__{server}__{tool}
|
|
260
|
+
const match = functionName.match(/^mcp__([^_]+(?:_[^_]+)*)__/);
|
|
261
|
+
return match ? match[1] : null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private extractToolFromFunctionName(functionName: string): string | null {
|
|
265
|
+
// Function name format: mcp__{server}__{tool}
|
|
266
|
+
const match = functionName.match(/^mcp__[^_]+(?:_[^_]+)*__(.+)$/);
|
|
267
|
+
return match ? match[1] : null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Check if a specific MCP tool is available
|
|
271
|
+
async isToolAvailable(serverName: string, toolName: string): Promise<boolean> {
|
|
272
|
+
try {
|
|
273
|
+
const tools = await this.mcpManager.listServerTools(serverName);
|
|
274
|
+
return tools.some(tool => tool.name === toolName);
|
|
275
|
+
} catch (error) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Get available MCP tools grouped by server
|
|
281
|
+
async getAvailableToolsByServer(): Promise<Record<string, MCPTool[]>> {
|
|
282
|
+
try {
|
|
283
|
+
const allTools = await this.mcpManager.listAllTools();
|
|
284
|
+
const toolsByServer: Record<string, MCPTool[]> = {};
|
|
285
|
+
|
|
286
|
+
allTools.forEach(tool => {
|
|
287
|
+
if (!toolsByServer[tool.serverName]) {
|
|
288
|
+
toolsByServer[tool.serverName] = [];
|
|
289
|
+
}
|
|
290
|
+
toolsByServer[tool.serverName].push(tool);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
return toolsByServer;
|
|
294
|
+
} catch (error: unknown) {
|
|
295
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
296
|
+
logger.error('Failed to get tools by server:', errorMessage);
|
|
297
|
+
return {};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Update trusted tools list
|
|
302
|
+
updateTrustedTools(trustedTools: string[], trustedToolsByServer?: Record<string, string[]>): void {
|
|
303
|
+
this.trustedTools = new Set(trustedTools);
|
|
304
|
+
logger.info(`Updated global trusted tools list: ${Array.from(this.trustedTools).join(', ')}`);
|
|
305
|
+
|
|
306
|
+
if (trustedToolsByServer) {
|
|
307
|
+
this.trustedToolsByServer.clear();
|
|
308
|
+
Object.entries(trustedToolsByServer).forEach(([serverName, tools]) => {
|
|
309
|
+
this.trustedToolsByServer.set(serverName, new Set(tools));
|
|
310
|
+
logger.info(`Updated trusted tools for server ${serverName}: ${tools.join(', ')}`);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Get statistics about function calls
|
|
316
|
+
getStats(): {
|
|
317
|
+
totalFunctions: number;
|
|
318
|
+
serverCount: number;
|
|
319
|
+
trustedToolCount: number;
|
|
320
|
+
connectedServers: string[];
|
|
321
|
+
} {
|
|
322
|
+
return {
|
|
323
|
+
totalFunctions: 0, // Will be populated when tools are loaded
|
|
324
|
+
serverCount: this.mcpManager.getConnectedServers().length,
|
|
325
|
+
trustedToolCount: this.trustedTools.size,
|
|
326
|
+
connectedServers: this.mcpManager.getConnectedServers(),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { ProviderManager } from '../providers/manager.js';
|
|
2
|
+
import { ProviderHealth } from '../config/types.js';
|
|
3
|
+
import { logger } from '../utils/logger.js';
|
|
4
|
+
|
|
5
|
+
export class HealthMonitor {
|
|
6
|
+
private providerManager: ProviderManager;
|
|
7
|
+
private healthCheckInterval: NodeJS.Timeout | null = null;
|
|
8
|
+
private healthCache: Map<string, ProviderHealth> = new Map();
|
|
9
|
+
|
|
10
|
+
constructor(providerManager: ProviderManager) {
|
|
11
|
+
this.providerManager = providerManager;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async performHealthChecks(): Promise<Map<string, ProviderHealth>> {
|
|
15
|
+
logger.info('🦆 Performing health checks on all ducks...');
|
|
16
|
+
|
|
17
|
+
const results = await this.providerManager.checkHealth();
|
|
18
|
+
|
|
19
|
+
for (const result of results) {
|
|
20
|
+
this.healthCache.set(result.provider, result);
|
|
21
|
+
|
|
22
|
+
const statusEmoji = result.healthy ? '✅' : '❌';
|
|
23
|
+
const latencyInfo = result.latency ? ` (${result.latency}ms)` : '';
|
|
24
|
+
|
|
25
|
+
logger.info(
|
|
26
|
+
`${statusEmoji} ${result.provider}: ${result.healthy ? 'Healthy' : 'Unhealthy'}${latencyInfo}`
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (result.error) {
|
|
30
|
+
logger.warn(` Error: ${result.error}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return this.healthCache;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
startMonitoring(intervalMs: number = 60000): void {
|
|
38
|
+
if (this.healthCheckInterval) {
|
|
39
|
+
this.stopMonitoring();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Initial check
|
|
43
|
+
this.performHealthChecks().catch(error => {
|
|
44
|
+
logger.error('Initial health check failed:', error);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Set up periodic checks
|
|
48
|
+
this.healthCheckInterval = setInterval(() => {
|
|
49
|
+
this.performHealthChecks().catch(error => {
|
|
50
|
+
logger.error('Periodic health check failed:', error);
|
|
51
|
+
});
|
|
52
|
+
}, intervalMs);
|
|
53
|
+
|
|
54
|
+
logger.info(`Started health monitoring with ${intervalMs}ms interval`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
stopMonitoring(): void {
|
|
58
|
+
if (this.healthCheckInterval) {
|
|
59
|
+
clearInterval(this.healthCheckInterval);
|
|
60
|
+
this.healthCheckInterval = null;
|
|
61
|
+
logger.info('Stopped health monitoring');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getHealthStatus(): Map<string, ProviderHealth> {
|
|
66
|
+
return new Map(this.healthCache);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
getHealthyProviders(): string[] {
|
|
70
|
+
return Array.from(this.healthCache.entries())
|
|
71
|
+
.filter(([_, health]) => health.healthy)
|
|
72
|
+
.map(([provider, _]) => provider);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
isProviderHealthy(providerName: string): boolean {
|
|
76
|
+
const health = this.healthCache.get(providerName);
|
|
77
|
+
return health?.healthy || false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getProviderLatency(providerName: string): number | undefined {
|
|
81
|
+
const health = this.healthCache.get(providerName);
|
|
82
|
+
return health?.latency;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async waitForHealthyProvider(
|
|
86
|
+
maxWaitMs: number = 30000,
|
|
87
|
+
checkIntervalMs: number = 1000
|
|
88
|
+
): Promise<string | null> {
|
|
89
|
+
const startTime = Date.now();
|
|
90
|
+
|
|
91
|
+
while (Date.now() - startTime < maxWaitMs) {
|
|
92
|
+
const healthyProviders = this.getHealthyProviders();
|
|
93
|
+
|
|
94
|
+
if (healthyProviders.length > 0) {
|
|
95
|
+
return healthyProviders[0];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Perform a fresh health check
|
|
99
|
+
await this.performHealthChecks();
|
|
100
|
+
|
|
101
|
+
// Wait before next check
|
|
102
|
+
await new Promise(resolve => setTimeout(resolve, checkIntervalMs));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|