morpheus-cli 0.1.5 → 0.1.6
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/README.md +266 -213
- package/bin/morpheus.js +30 -0
- package/dist/channels/telegram.js +2 -2
- package/dist/cli/commands/config.js +18 -3
- package/dist/cli/commands/init.js +3 -3
- package/dist/cli/index.js +4 -0
- package/dist/config/mcp-loader.js +42 -0
- package/dist/config/schemas.js +13 -0
- package/dist/http/__tests__/auth.test.js +53 -0
- package/dist/http/api.js +12 -0
- package/dist/http/middleware/auth.js +23 -0
- package/dist/http/server.js +2 -1
- package/dist/runtime/__tests__/manual_start_verify.js +1 -0
- package/dist/runtime/agent.js +79 -7
- package/dist/runtime/audio-agent.js +11 -1
- package/dist/runtime/display.js +12 -0
- package/dist/runtime/memory/sqlite.js +139 -9
- package/dist/runtime/providers/factory.js +27 -2
- package/dist/runtime/scaffold.js +5 -0
- package/dist/runtime/tools/__tests__/tools.test.js +127 -0
- package/dist/runtime/tools/analytics-tools.js +73 -0
- package/dist/runtime/tools/config-tools.js +70 -0
- package/dist/runtime/tools/diagnostic-tools.js +124 -0
- package/dist/runtime/tools/factory.js +55 -11
- package/dist/runtime/tools/index.js +4 -0
- package/dist/types/auth.js +4 -0
- package/dist/types/config.js +1 -0
- package/dist/types/mcp.js +11 -0
- package/dist/types/tools.js +2 -0
- package/dist/types/usage.js +1 -0
- package/dist/ui/assets/index-4kQpg2wK.js +50 -0
- package/dist/ui/assets/index-CwL7mn36.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +2 -1
- package/dist/ui/assets/index-D1kvj0eG.css +0 -1
- package/dist/ui/assets/index-DTh8waF7.js +0 -50
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { ConfigQueryTool, ConfigUpdateTool } from '../config-tools.js';
|
|
3
|
+
import { DiagnosticTool } from '../diagnostic-tools.js';
|
|
4
|
+
import { MessageCountTool, TokenUsageTool } from '../analytics-tools.js';
|
|
5
|
+
import { ConfigManager } from '../../../config/manager.js';
|
|
6
|
+
// Mock the ConfigManager for testing
|
|
7
|
+
vi.mock('../../config/manager.js', () => ({
|
|
8
|
+
ConfigManager: {
|
|
9
|
+
getInstance: vi.fn(() => ({
|
|
10
|
+
load: vi.fn(async () => ({
|
|
11
|
+
llm: { provider: 'openai', model: 'gpt-4' },
|
|
12
|
+
logging: { enabled: true, level: 'info' },
|
|
13
|
+
ui: { enabled: true, port: 3000 }
|
|
14
|
+
})),
|
|
15
|
+
get: vi.fn(() => ({
|
|
16
|
+
llm: { provider: 'openai', model: 'gpt-4' },
|
|
17
|
+
logging: { enabled: true, level: 'info' },
|
|
18
|
+
ui: { enabled: true, port: 3000 }
|
|
19
|
+
})),
|
|
20
|
+
save: vi.fn(async (newConfig) => {
|
|
21
|
+
// Mock save implementation
|
|
22
|
+
})
|
|
23
|
+
}))
|
|
24
|
+
}
|
|
25
|
+
}));
|
|
26
|
+
describe('Config Tools', () => {
|
|
27
|
+
describe('ConfigQueryTool', () => {
|
|
28
|
+
it('should query all configuration values when no key is provided', async () => {
|
|
29
|
+
const result = await ConfigQueryTool.invoke({});
|
|
30
|
+
const parsedResult = JSON.parse(result);
|
|
31
|
+
expect(parsedResult).toHaveProperty('llm');
|
|
32
|
+
expect(parsedResult).toHaveProperty('logging');
|
|
33
|
+
expect(parsedResult).toHaveProperty('ui');
|
|
34
|
+
});
|
|
35
|
+
it('should query specific configuration value when key is provided', async () => {
|
|
36
|
+
const result = await ConfigQueryTool.invoke({ key: 'llm' });
|
|
37
|
+
const parsedResult = JSON.parse(result);
|
|
38
|
+
expect(parsedResult).toHaveProperty('llm');
|
|
39
|
+
const llmConfig = parsedResult.llm;
|
|
40
|
+
expect(llmConfig).toHaveProperty('provider');
|
|
41
|
+
expect(llmConfig).toHaveProperty('model');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
describe('ConfigUpdateTool', () => {
|
|
45
|
+
it('should update configuration values', async () => {
|
|
46
|
+
const updates = { 'ui.port': 4000 };
|
|
47
|
+
const result = await ConfigUpdateTool.invoke({ updates });
|
|
48
|
+
const parsedResult = JSON.parse(result);
|
|
49
|
+
expect(parsedResult).toHaveProperty('success', true);
|
|
50
|
+
});
|
|
51
|
+
it('should return error when update fails', async () => {
|
|
52
|
+
// Create a new mock instance for this specific test
|
|
53
|
+
const originalGetInstance = ConfigManager.getInstance;
|
|
54
|
+
const mockGetInstance = vi.fn(() => ({
|
|
55
|
+
load: vi.fn(async () => ({
|
|
56
|
+
llm: { provider: 'openai', model: 'gpt-4' },
|
|
57
|
+
logging: { enabled: true, level: 'info' },
|
|
58
|
+
ui: { enabled: true, port: 3000 }
|
|
59
|
+
})),
|
|
60
|
+
get: vi.fn(() => ({
|
|
61
|
+
llm: { provider: 'openai', model: 'gpt-4' },
|
|
62
|
+
logging: { enabled: true, level: 'info' },
|
|
63
|
+
ui: { enabled: true, port: 3000 }
|
|
64
|
+
})),
|
|
65
|
+
save: vi.fn(async () => {
|
|
66
|
+
throw new Error('Save failed');
|
|
67
|
+
})
|
|
68
|
+
}));
|
|
69
|
+
// Replace the getInstance method temporarily
|
|
70
|
+
ConfigManager.getInstance = mockGetInstance;
|
|
71
|
+
const updates = { 'invalid.field': 'value' };
|
|
72
|
+
const result = await ConfigUpdateTool.invoke({ updates });
|
|
73
|
+
const parsedResult = JSON.parse(result);
|
|
74
|
+
expect(parsedResult).toHaveProperty('error');
|
|
75
|
+
// Restore the original method
|
|
76
|
+
ConfigManager.getInstance = originalGetInstance;
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe('DiagnosticTool', () => {
|
|
81
|
+
it('should return a diagnostic report with component statuses', async () => {
|
|
82
|
+
const result = await DiagnosticTool.invoke({});
|
|
83
|
+
const parsedResult = JSON.parse(result);
|
|
84
|
+
expect(parsedResult).toHaveProperty('timestamp');
|
|
85
|
+
expect(parsedResult).toHaveProperty('components');
|
|
86
|
+
expect(parsedResult.components).toHaveProperty('config');
|
|
87
|
+
expect(parsedResult.components).toHaveProperty('storage');
|
|
88
|
+
expect(parsedResult.components).toHaveProperty('network');
|
|
89
|
+
expect(parsedResult.components).toHaveProperty('agent');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe('Analytics Tools', () => {
|
|
93
|
+
// Note: These tools now use the existing SQLite class and access the database
|
|
94
|
+
describe('MessageCountTool', () => {
|
|
95
|
+
it('should return message count', async () => {
|
|
96
|
+
const result = await MessageCountTool.invoke({});
|
|
97
|
+
const parsedResult = JSON.parse(result);
|
|
98
|
+
// Should return a number (message count) or an error if database doesn't exist
|
|
99
|
+
if (typeof parsedResult === 'object' && parsedResult.error) {
|
|
100
|
+
// If there's an error (e.g., database doesn't exist), that's acceptable in test environment
|
|
101
|
+
expect(parsedResult).toHaveProperty('error');
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// Otherwise, should return a number
|
|
105
|
+
expect(typeof parsedResult).toBe('number');
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
describe('TokenUsageTool', () => {
|
|
110
|
+
it('should return token usage statistics', async () => {
|
|
111
|
+
const result = await TokenUsageTool.invoke({});
|
|
112
|
+
const parsedResult = JSON.parse(result);
|
|
113
|
+
// Should return token statistics or an error if database doesn't exist
|
|
114
|
+
if (typeof parsedResult === 'object' && parsedResult.error) {
|
|
115
|
+
// If there's an error (e.g., database doesn't exist), that's acceptable in test environment
|
|
116
|
+
expect(parsedResult).toHaveProperty('error');
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
// Otherwise, should return token statistics
|
|
120
|
+
expect(parsedResult).toHaveProperty('totalTokens');
|
|
121
|
+
expect(parsedResult).toHaveProperty('inputTokens');
|
|
122
|
+
expect(parsedResult).toHaveProperty('outputTokens');
|
|
123
|
+
expect(parsedResult).toHaveProperty('timestamp');
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { tool } from "langchain";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import Database from "better-sqlite3";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
// Tool for querying message counts from the database
|
|
7
|
+
const dbPath = path.join(homedir(), ".morpheus", "memory", "short-memory.db");
|
|
8
|
+
export const MessageCountTool = tool(async ({ timeRange }) => {
|
|
9
|
+
try {
|
|
10
|
+
// Connect to database
|
|
11
|
+
const db = new Database(dbPath);
|
|
12
|
+
let query = "SELECT COUNT(*) as count FROM messages";
|
|
13
|
+
const params = [];
|
|
14
|
+
if (timeRange) {
|
|
15
|
+
query += " WHERE timestamp BETWEEN ? AND ?";
|
|
16
|
+
params.push(timeRange.start);
|
|
17
|
+
params.push(timeRange.end);
|
|
18
|
+
}
|
|
19
|
+
const result = db.prepare(query).get(params);
|
|
20
|
+
db.close();
|
|
21
|
+
return JSON.stringify(result.count);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
console.error("Error in MessageCountTool:", error);
|
|
25
|
+
return JSON.stringify({ error: `Failed to count messages: ${error.message}` });
|
|
26
|
+
}
|
|
27
|
+
}, {
|
|
28
|
+
name: "message_count",
|
|
29
|
+
description: "Returns count of stored messages. Accepts an optional 'timeRange' parameter with start and end timestamps for filtering.",
|
|
30
|
+
schema: z.object({
|
|
31
|
+
timeRange: z.object({
|
|
32
|
+
start: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/, "ISO date string"),
|
|
33
|
+
end: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/, "ISO date string"),
|
|
34
|
+
}).optional(),
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
// Tool for querying token usage statistics from the database
|
|
38
|
+
export const TokenUsageTool = tool(async ({ timeRange }) => {
|
|
39
|
+
try {
|
|
40
|
+
// Connect to database
|
|
41
|
+
const db = new Database(dbPath);
|
|
42
|
+
let query = "SELECT SUM(input_tokens) as inputTokens, SUM(output_tokens) as outputTokens, SUM(input_tokens + output_tokens) as totalTokens FROM messages";
|
|
43
|
+
const params = [];
|
|
44
|
+
if (timeRange) {
|
|
45
|
+
query += " WHERE timestamp BETWEEN ? AND ?";
|
|
46
|
+
params.push(timeRange.start);
|
|
47
|
+
params.push(timeRange.end);
|
|
48
|
+
}
|
|
49
|
+
const result = db.prepare(query).get(params);
|
|
50
|
+
db.close();
|
|
51
|
+
// Handle potential null values
|
|
52
|
+
const tokenStats = {
|
|
53
|
+
totalTokens: result.totalTokens || 0,
|
|
54
|
+
inputTokens: result.inputTokens || 0,
|
|
55
|
+
outputTokens: result.outputTokens || 0,
|
|
56
|
+
timestamp: new Date().toISOString()
|
|
57
|
+
};
|
|
58
|
+
return JSON.stringify(tokenStats);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
console.error("Error in TokenUsageTool:", error);
|
|
62
|
+
return JSON.stringify({ error: `Failed to get token usage: ${error.message}` });
|
|
63
|
+
}
|
|
64
|
+
}, {
|
|
65
|
+
name: "token_usage",
|
|
66
|
+
description: "Returns token usage statistics. Accepts an optional 'timeRange' parameter with start and end timestamps for filtering.",
|
|
67
|
+
schema: z.object({
|
|
68
|
+
timeRange: z.object({
|
|
69
|
+
start: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/, "ISO date string"),
|
|
70
|
+
end: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/, "ISO date string"),
|
|
71
|
+
}).optional(),
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { tool } from "langchain";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { ConfigManager } from "../../config/manager.js";
|
|
4
|
+
// Tool for querying configuration values
|
|
5
|
+
export const ConfigQueryTool = tool(async ({ key }) => {
|
|
6
|
+
try {
|
|
7
|
+
const configManager = ConfigManager.getInstance();
|
|
8
|
+
// Load config if not already loaded
|
|
9
|
+
await configManager.load();
|
|
10
|
+
const config = configManager.get();
|
|
11
|
+
if (key) {
|
|
12
|
+
// Return specific configuration value
|
|
13
|
+
const value = config[key];
|
|
14
|
+
return JSON.stringify({ [key]: value });
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
// Return all configuration values
|
|
18
|
+
return JSON.stringify(config);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
console.error("Error in ConfigQueryTool:", error);
|
|
23
|
+
return JSON.stringify({ error: "Failed to query configuration" });
|
|
24
|
+
}
|
|
25
|
+
}, {
|
|
26
|
+
name: "config_query",
|
|
27
|
+
description: "Queries current configuration values. Accepts an optional 'key' parameter to get a specific configuration value, or no parameter to get all configuration values.",
|
|
28
|
+
schema: z.object({
|
|
29
|
+
key: z.string().optional(),
|
|
30
|
+
}),
|
|
31
|
+
});
|
|
32
|
+
// Tool for updating configuration values
|
|
33
|
+
export const ConfigUpdateTool = tool(async ({ updates }) => {
|
|
34
|
+
try {
|
|
35
|
+
const configManager = ConfigManager.getInstance();
|
|
36
|
+
// Load current config
|
|
37
|
+
await configManager.load();
|
|
38
|
+
const currentConfig = configManager.get();
|
|
39
|
+
// Create new config with updates
|
|
40
|
+
const newConfig = { ...currentConfig, ...updates };
|
|
41
|
+
// Save the updated config
|
|
42
|
+
await configManager.save(newConfig);
|
|
43
|
+
return JSON.stringify({ success: true, message: "Configuration updated successfully" });
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
console.error("Error in ConfigUpdateTool:", error);
|
|
47
|
+
return JSON.stringify({ error: `Failed to update configuration: ${error.message}` });
|
|
48
|
+
}
|
|
49
|
+
}, {
|
|
50
|
+
name: "config_update",
|
|
51
|
+
description: "Updates configuration values with validation. Accepts an 'updates' object containing key-value pairs to update.",
|
|
52
|
+
schema: z.object({
|
|
53
|
+
updates: z.object({
|
|
54
|
+
// Define common config properties that might be updated
|
|
55
|
+
// Using optional fields to allow flexible updates
|
|
56
|
+
"llm.provider": z.string().optional(),
|
|
57
|
+
"llm.model": z.string().optional(),
|
|
58
|
+
"llm.temperature": z.number().optional(),
|
|
59
|
+
"llm.api_key": z.string().optional(),
|
|
60
|
+
"ui.enabled": z.boolean().optional(),
|
|
61
|
+
"ui.port": z.number().optional(),
|
|
62
|
+
"logging.enabled": z.boolean().optional(),
|
|
63
|
+
"logging.level": z.enum(['debug', 'info', 'warn', 'error']).optional(),
|
|
64
|
+
"audio.enabled": z.boolean().optional(),
|
|
65
|
+
"audio.provider": z.string().optional(),
|
|
66
|
+
"memory.limit": z.number().optional(),
|
|
67
|
+
// Add more specific fields as needed, or use a catch-all for other properties
|
|
68
|
+
}).passthrough(), // Allow additional properties not explicitly defined
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { tool } from "langchain";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { ConfigManager } from "../../config/manager.js";
|
|
4
|
+
import { promises as fsPromises } from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
// Tool for performing system diagnostics
|
|
8
|
+
export const DiagnosticTool = tool(async () => {
|
|
9
|
+
try {
|
|
10
|
+
const timestamp = new Date().toISOString();
|
|
11
|
+
const components = {};
|
|
12
|
+
// Check configuration
|
|
13
|
+
try {
|
|
14
|
+
const configManager = ConfigManager.getInstance();
|
|
15
|
+
await configManager.load();
|
|
16
|
+
const config = configManager.get();
|
|
17
|
+
// Basic validation - check if required fields exist
|
|
18
|
+
const requiredFields = ['llm', 'logging', 'ui'];
|
|
19
|
+
const missingFields = requiredFields.filter(field => !(field in config));
|
|
20
|
+
if (missingFields.length === 0) {
|
|
21
|
+
components.config = {
|
|
22
|
+
status: "healthy",
|
|
23
|
+
message: "Configuration is valid and complete",
|
|
24
|
+
details: {
|
|
25
|
+
llmProvider: config.llm?.provider,
|
|
26
|
+
uiEnabled: config.ui?.enabled,
|
|
27
|
+
uiPort: config.ui?.port
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
components.config = {
|
|
33
|
+
status: "warning",
|
|
34
|
+
message: `Missing required configuration fields: ${missingFields.join(', ')}`,
|
|
35
|
+
details: { missingFields }
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
components.config = {
|
|
41
|
+
status: "error",
|
|
42
|
+
message: `Configuration error: ${error.message}`,
|
|
43
|
+
details: {}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// Check storage/database
|
|
47
|
+
try {
|
|
48
|
+
// For now, we'll check if the data directory exists
|
|
49
|
+
const dbPath = path.join(homedir(), ".morpheus", "memory", "short-memory.db");
|
|
50
|
+
await fsPromises.access(dbPath);
|
|
51
|
+
components.storage = {
|
|
52
|
+
status: "healthy",
|
|
53
|
+
message: "Database file is accessible",
|
|
54
|
+
details: { path: dbPath }
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
components.storage = {
|
|
59
|
+
status: "error",
|
|
60
|
+
message: `Storage error: ${error.message}`,
|
|
61
|
+
details: {}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// Check network connectivity (basic check)
|
|
65
|
+
try {
|
|
66
|
+
// For now, we'll just check if we can reach the LLM provider configuration
|
|
67
|
+
const configManager = ConfigManager.getInstance();
|
|
68
|
+
await configManager.load();
|
|
69
|
+
const config = configManager.get();
|
|
70
|
+
if (config.llm && config.llm.provider) {
|
|
71
|
+
components.network = {
|
|
72
|
+
status: "healthy",
|
|
73
|
+
message: `LLM provider configured: ${config.llm.provider}`,
|
|
74
|
+
details: { provider: config.llm.provider }
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
components.network = {
|
|
79
|
+
status: "warning",
|
|
80
|
+
message: "No LLM provider configured",
|
|
81
|
+
details: {}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
components.network = {
|
|
87
|
+
status: "error",
|
|
88
|
+
message: `Network check error: ${error.message}`,
|
|
89
|
+
details: {}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// Check if the agent is running
|
|
93
|
+
try {
|
|
94
|
+
// This is a basic check - in a real implementation, we might check if the agent process is running
|
|
95
|
+
components.agent = {
|
|
96
|
+
status: "healthy",
|
|
97
|
+
message: "Agent is running",
|
|
98
|
+
details: { uptime: "N/A - runtime information not available in this context" }
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
components.agent = {
|
|
103
|
+
status: "error",
|
|
104
|
+
message: `Agent check error: ${error.message}`,
|
|
105
|
+
details: {}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return JSON.stringify({
|
|
109
|
+
timestamp,
|
|
110
|
+
components
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
console.error("Error in DiagnosticTool:", error);
|
|
115
|
+
return JSON.stringify({
|
|
116
|
+
timestamp: new Date().toISOString(),
|
|
117
|
+
error: "Failed to run diagnostics"
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}, {
|
|
121
|
+
name: "diagnostic_check",
|
|
122
|
+
description: "Performs system health diagnostics and returns a comprehensive report on system components.",
|
|
123
|
+
schema: z.object({}),
|
|
124
|
+
});
|
|
@@ -1,33 +1,77 @@
|
|
|
1
1
|
import { MultiServerMCPClient } from "@langchain/mcp-adapters";
|
|
2
2
|
import { DisplayManager } from "../display.js";
|
|
3
|
+
import { loadMCPConfig } from "../../config/mcp-loader.js";
|
|
4
|
+
const display = DisplayManager.getInstance();
|
|
5
|
+
// Fields not supported by Google Gemini API
|
|
6
|
+
const UNSUPPORTED_SCHEMA_FIELDS = ['examples', 'additionalInfo', 'default', '$schema'];
|
|
7
|
+
/**
|
|
8
|
+
* Recursively removes unsupported fields from JSON schema objects.
|
|
9
|
+
* This is needed because some MCP servers (like Coolify) return schemas
|
|
10
|
+
* with fields that Gemini doesn't accept.
|
|
11
|
+
*/
|
|
12
|
+
function sanitizeSchema(obj) {
|
|
13
|
+
if (obj === null || typeof obj !== 'object') {
|
|
14
|
+
return obj;
|
|
15
|
+
}
|
|
16
|
+
if (Array.isArray(obj)) {
|
|
17
|
+
return obj.map(sanitizeSchema);
|
|
18
|
+
}
|
|
19
|
+
const sanitized = {};
|
|
20
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
21
|
+
if (!UNSUPPORTED_SCHEMA_FIELDS.includes(key)) {
|
|
22
|
+
sanitized[key] = sanitizeSchema(value);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return sanitized;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Wraps a tool to sanitize its schema for Gemini compatibility.
|
|
29
|
+
* Creates a proxy that intercepts schema access and sanitizes the output.
|
|
30
|
+
*/
|
|
31
|
+
function wrapToolWithSanitizedSchema(tool) {
|
|
32
|
+
display.log('Tool loaded: - ' + tool.name, { source: 'ToolsFactory' });
|
|
33
|
+
// The MCP tools have a schema property that returns JSON Schema
|
|
34
|
+
// We need to intercept and sanitize it
|
|
35
|
+
const originalSchema = tool.schema;
|
|
36
|
+
if (originalSchema && typeof originalSchema === 'object') {
|
|
37
|
+
// Sanitize the schema object directly
|
|
38
|
+
const sanitized = sanitizeSchema(originalSchema);
|
|
39
|
+
tool.schema = sanitized;
|
|
40
|
+
}
|
|
41
|
+
return tool;
|
|
42
|
+
}
|
|
3
43
|
export class ToolsFactory {
|
|
4
44
|
static async create() {
|
|
5
45
|
const display = DisplayManager.getInstance();
|
|
46
|
+
const mcpServers = await loadMCPConfig();
|
|
47
|
+
const serverCount = Object.keys(mcpServers).length;
|
|
48
|
+
if (serverCount === 0) {
|
|
49
|
+
display.log('No MCP servers configured in mcps.json', { level: 'info', source: 'ToolsFactory' });
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
6
52
|
const client = new MultiServerMCPClient({
|
|
7
|
-
mcpServers:
|
|
8
|
-
|
|
9
|
-
transport: "stdio",
|
|
10
|
-
command: "npx",
|
|
11
|
-
args: ["-y", "mcp_coingecko_price_ts"],
|
|
12
|
-
},
|
|
13
|
-
},
|
|
53
|
+
mcpServers: mcpServers,
|
|
54
|
+
onConnectionError: "ignore",
|
|
14
55
|
// log the MCP client's internal events
|
|
15
56
|
beforeToolCall: ({ serverName, name, args }) => {
|
|
16
|
-
display.log(`MCP Tool Call - Server: ${serverName}, Tool: ${name}, Args: ${JSON.stringify(args)}
|
|
57
|
+
display.log(`MCP Tool Call - Server: ${serverName}, Tool: ${name}, Args: ${JSON.stringify(args)}`, { source: 'MCPServer' });
|
|
17
58
|
return;
|
|
18
59
|
},
|
|
19
60
|
// log the results of tool calls
|
|
20
61
|
afterToolCall: (res) => {
|
|
21
|
-
display.log(`MCP Tool Result - ${JSON.stringify(res)}
|
|
62
|
+
display.log(`MCP Tool Result - ${JSON.stringify(res)}`, { source: 'MCPServer' });
|
|
22
63
|
return;
|
|
23
64
|
}
|
|
24
65
|
});
|
|
25
66
|
try {
|
|
26
67
|
const tools = await client.getTools();
|
|
27
|
-
|
|
68
|
+
// Sanitize tool schemas to remove fields not supported by Gemini
|
|
69
|
+
const sanitizedTools = tools.map(tool => wrapToolWithSanitizedSchema(tool));
|
|
70
|
+
display.log(`Loaded ${sanitizedTools.length} MCP tools (schemas sanitized for Gemini compatibility)`, { level: 'info', source: 'ToolsFactory' });
|
|
71
|
+
return sanitizedTools;
|
|
28
72
|
}
|
|
29
73
|
catch (error) {
|
|
30
|
-
display.log(`Failed to initialize MCP tools: ${error}`, { level: 'warning' });
|
|
74
|
+
display.log(`Failed to initialize MCP tools: ${error}`, { level: 'warning', source: 'ToolsFactory' });
|
|
31
75
|
return []; // Return empty tools on failure to allow agent to start
|
|
32
76
|
}
|
|
33
77
|
}
|
package/dist/types/config.js
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const DEFAULT_MCP_TEMPLATE = {
|
|
2
|
+
"_comment": "MCP Server Configuration for Morpheus",
|
|
3
|
+
"_docs": "Add your MCP servers below. Each key is a unique server name.",
|
|
4
|
+
"example": {
|
|
5
|
+
"_comment": "EXAMPLE - Remove or replace this entry with your own MCPs",
|
|
6
|
+
"transport": "stdio",
|
|
7
|
+
"command": "npx",
|
|
8
|
+
"args": ["-y", "your-mcp-package-name"],
|
|
9
|
+
"env": {}
|
|
10
|
+
}
|
|
11
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|