vibe-weaver 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.
- package/README.md +229 -0
- package/bin/vibe-weaver.js +8 -0
- package/dist/bin/vibe-weaver.js +8 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2018 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
- package/src/agent/deploy.ts +171 -0
- package/src/agent/swarm.ts +167 -0
- package/src/agent/tasks.ts +205 -0
- package/src/auth/index.ts +273 -0
- package/src/config/index.ts +86 -0
- package/src/index.ts +797 -0
- package/src/lib/code-parser.ts +150 -0
- package/src/lib/vibe-api.ts +197 -0
- package/src/mcp/index.ts +273 -0
- package/src/tools/files.ts +279 -0
- package/src/tools/git.ts +498 -0
- package/src/tools/shell.ts +248 -0
- package/tsconfig.json +25 -0
- package/tsup.config.ts +34 -0
- package/vibe-weaver-1.0.0.tgz +0 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Block Parser for Vibe-Weaver CLI
|
|
3
|
+
* Parses markdown code blocks with language and file path
|
|
4
|
+
* Format: ```lang path\n...```
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface ParsedCodeBlock {
|
|
8
|
+
language: string;
|
|
9
|
+
path: string;
|
|
10
|
+
content: string;
|
|
11
|
+
startLine: number;
|
|
12
|
+
endLine: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ParseResult {
|
|
16
|
+
blocks: ParsedCodeBlock[];
|
|
17
|
+
explanation?: string;
|
|
18
|
+
quality?: {
|
|
19
|
+
score: number;
|
|
20
|
+
issues: string[];
|
|
21
|
+
suggestions: string[];
|
|
22
|
+
};
|
|
23
|
+
stats?: {
|
|
24
|
+
mode: string;
|
|
25
|
+
parameters: string;
|
|
26
|
+
architecture: string;
|
|
27
|
+
inferenceTimeMs: number;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parse code blocks from the AI response
|
|
33
|
+
* Supports format: ```typescript src/App.tsx\n...content...```
|
|
34
|
+
*/
|
|
35
|
+
export function parseCodeBlocks(response: string): ParseResult {
|
|
36
|
+
const blocks: ParsedCodeBlock[] = [];
|
|
37
|
+
|
|
38
|
+
// Regex to match code blocks with optional language and path
|
|
39
|
+
// Format: ```[lang[ path]]\n...content...\n```
|
|
40
|
+
const codeBlockRegex = /```(\w+)?\s+(.+?)\n([\s\S]*?)```/g;
|
|
41
|
+
|
|
42
|
+
let match;
|
|
43
|
+
while ((match = codeBlockRegex.exec(response)) !== null) {
|
|
44
|
+
const language = match[1] || 'text';
|
|
45
|
+
const path = match[2].trim();
|
|
46
|
+
const content = match[3];
|
|
47
|
+
|
|
48
|
+
// Calculate line numbers
|
|
49
|
+
const beforeBlock = response.substring(0, match.index);
|
|
50
|
+
const startLine = (beforeBlock.match(/\n/g) || []).length + 1;
|
|
51
|
+
const endLine = startLine + (content.match(/\n/g) || []).length;
|
|
52
|
+
|
|
53
|
+
blocks.push({
|
|
54
|
+
language,
|
|
55
|
+
path,
|
|
56
|
+
content,
|
|
57
|
+
startLine,
|
|
58
|
+
endLine
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Try to extract explanation (text before first code block)
|
|
63
|
+
let explanation: string | undefined;
|
|
64
|
+
const firstCodeBlockIndex = response.indexOf('```');
|
|
65
|
+
if (firstCodeBlockIndex > 0) {
|
|
66
|
+
explanation = response.substring(0, firstCodeBlockIndex).trim();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { blocks, explanation };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parse a single code block string
|
|
74
|
+
*/
|
|
75
|
+
export function parseSingleCodeBlock(blockStr: string): { language: string; path: string; content: string } | null {
|
|
76
|
+
const match = blockStr.match(/^(\w+)?\s+(.+?)\n([\s\S]*)$/);
|
|
77
|
+
if (!match) return null;
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
language: match[1] || 'text',
|
|
81
|
+
path: match[2].trim(),
|
|
82
|
+
content: match[3]
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Write parsed code blocks to disk
|
|
88
|
+
*/
|
|
89
|
+
export async function writeCodeBlocks(
|
|
90
|
+
blocks: ParsedCodeBlock[],
|
|
91
|
+
baseDir: string,
|
|
92
|
+
options: { dryRun?: boolean } = {}
|
|
93
|
+
): Promise<{ success: string[]; failed: { path: string; error: string }[] }> {
|
|
94
|
+
const fs = await import('fs/promises');
|
|
95
|
+
const path = await import('path');
|
|
96
|
+
|
|
97
|
+
const success: string[] = [];
|
|
98
|
+
const failed: { path: string; error: string }[] = [];
|
|
99
|
+
|
|
100
|
+
for (const block of blocks) {
|
|
101
|
+
try {
|
|
102
|
+
const fullPath = path.join(baseDir, block.path);
|
|
103
|
+
const dir = path.dirname(fullPath);
|
|
104
|
+
|
|
105
|
+
if (!options.dryRun) {
|
|
106
|
+
// Create directory if it doesn't exist
|
|
107
|
+
await fs.mkdir(dir, { recursive: true });
|
|
108
|
+
|
|
109
|
+
// Write file
|
|
110
|
+
await fs.writeFile(fullPath, block.content, 'utf-8');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
success.push(block.path);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
failed.push({
|
|
116
|
+
path: block.path,
|
|
117
|
+
error: error instanceof Error ? error.message : String(error)
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { success, failed };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Generate a diff between old and new content
|
|
127
|
+
*/
|
|
128
|
+
export function generateDiff(oldContent: string, newContent: string, filePath: string): string {
|
|
129
|
+
const diff = require('diff');
|
|
130
|
+
const changes = diff.diffLines(oldContent, newContent);
|
|
131
|
+
|
|
132
|
+
let result = `--- ${filePath}\n+++ ${filePath}\n`;
|
|
133
|
+
|
|
134
|
+
for (const change of changes) {
|
|
135
|
+
const prefix = change.added ? '+' : change.removed ? '-' : ' ';
|
|
136
|
+
const lines = change.value.split('\n').filter((l: string) => l.length > 0);
|
|
137
|
+
for (const line of lines) {
|
|
138
|
+
result += `${prefix}${line}\n`;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export default {
|
|
146
|
+
parseCodeBlocks,
|
|
147
|
+
parseSingleCodeBlock,
|
|
148
|
+
writeCodeBlocks,
|
|
149
|
+
generateDiff
|
|
150
|
+
};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vibe-Weaver API Client
|
|
3
|
+
* Communicates with the vibe-ai-api edge function
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getApiKey, getGithubToken } from '../auth/index.js';
|
|
7
|
+
import { getSupabaseUrl } from '../config/index.js';
|
|
8
|
+
import { parseCodeBlocks, ParseResult } from './code-parser.js';
|
|
9
|
+
|
|
10
|
+
export type ModelId = 'fast' | 'balanced' | 'deep_reasoning' | 'refactor';
|
|
11
|
+
export type Platform = 'web' | 'roblox' | 'mobile' | 'desktop' | 'api';
|
|
12
|
+
export type Language = 'typescript' | 'javascript' | 'python' | 'lua' | 'rust';
|
|
13
|
+
|
|
14
|
+
export interface GenerateOptions {
|
|
15
|
+
goal: string;
|
|
16
|
+
platform?: Platform;
|
|
17
|
+
language?: Language;
|
|
18
|
+
modelId?: ModelId;
|
|
19
|
+
context?: string;
|
|
20
|
+
stream?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface GenerateResult {
|
|
24
|
+
code: string;
|
|
25
|
+
explanation: string;
|
|
26
|
+
quality: {
|
|
27
|
+
score: number;
|
|
28
|
+
issues: string[];
|
|
29
|
+
suggestions: string[];
|
|
30
|
+
};
|
|
31
|
+
stats: {
|
|
32
|
+
mode: string;
|
|
33
|
+
parameters: string;
|
|
34
|
+
architecture: string;
|
|
35
|
+
inferenceTimeMs: number;
|
|
36
|
+
};
|
|
37
|
+
parsed: ParseResult;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface CreditInfo {
|
|
41
|
+
cost: number;
|
|
42
|
+
remaining: number;
|
|
43
|
+
plan: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get credit cost for a given model
|
|
48
|
+
*/
|
|
49
|
+
export function getCreditCost(modelId: ModelId): number {
|
|
50
|
+
const costs: Record<ModelId, number> = {
|
|
51
|
+
'fast': 0.2,
|
|
52
|
+
'balanced': 0.5,
|
|
53
|
+
'deep_reasoning': 1.0,
|
|
54
|
+
'refactor': 1.0
|
|
55
|
+
};
|
|
56
|
+
return costs[modelId] || 0.5;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Call the vibe-ai-api to generate code
|
|
61
|
+
*/
|
|
62
|
+
export async function generateCode(options: GenerateOptions): Promise<GenerateResult> {
|
|
63
|
+
const apiKey = await getApiKey();
|
|
64
|
+
if (!apiKey) {
|
|
65
|
+
throw new Error('Not logged in. Run "vibe-weaver login" first.');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const supabaseUrl = getSupabaseUrl();
|
|
69
|
+
const endpoint = `${supabaseUrl}/functions/v1/vibe-ai-api`;
|
|
70
|
+
|
|
71
|
+
const payload = {
|
|
72
|
+
goal: options.goal,
|
|
73
|
+
platform: options.platform || 'web',
|
|
74
|
+
language: options.language || 'typescript',
|
|
75
|
+
modelId: options.modelId || 'balanced'
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
console.log(`\n${'[Vibe-Weaver]'.padEnd(20)} Calling ${payload.modelId.toUpperCase()} model for: ${options.goal.substring(0, 50)}...`);
|
|
79
|
+
|
|
80
|
+
const response = await fetch(endpoint, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: {
|
|
83
|
+
'Content-Type': 'application/json',
|
|
84
|
+
'Authorization': `Bearer ${apiKey}`
|
|
85
|
+
},
|
|
86
|
+
body: JSON.stringify(payload)
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
91
|
+
|
|
92
|
+
if (response.status === 401) {
|
|
93
|
+
throw new Error('Authentication failed. Please run "vibe-weaver login" again.');
|
|
94
|
+
}
|
|
95
|
+
if (response.status === 402) {
|
|
96
|
+
throw new Error(`Insufficient credits: ${error.message}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
throw new Error(`API error: ${error.error || error.message}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const result = await response.json();
|
|
103
|
+
|
|
104
|
+
// Parse code blocks from the response
|
|
105
|
+
const parsed = parseCodeBlocks(result.code);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
code: result.code,
|
|
109
|
+
explanation: result.explanation || '',
|
|
110
|
+
quality: result.quality || { score: 0, issues: [], suggestions: [] },
|
|
111
|
+
stats: result.stats || { mode: payload.modelId, parameters: 'N/A', architecture: 'N/A', inferenceTimeMs: 0 },
|
|
112
|
+
parsed
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Stream code generation (for future implementation)
|
|
118
|
+
*/
|
|
119
|
+
export async function* streamGenerate(options: GenerateOptions): AsyncGenerator<{ delta: string; done: boolean }> {
|
|
120
|
+
const apiKey = await getApiKey();
|
|
121
|
+
if (!apiKey) {
|
|
122
|
+
throw new Error('Not logged in. Run "vibe-weaver login" first.');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const supabaseUrl = getSupabaseUrl();
|
|
126
|
+
const endpoint = `${supabaseUrl}/functions/v1/vibe-ai-api`;
|
|
127
|
+
|
|
128
|
+
const payload = {
|
|
129
|
+
goal: options.goal,
|
|
130
|
+
platform: options.platform || 'web',
|
|
131
|
+
language: options.language || 'typescript',
|
|
132
|
+
modelId: options.modelId || 'balanced'
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const response = await fetch(endpoint, {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: {
|
|
138
|
+
'Content-Type': 'application/json',
|
|
139
|
+
'Authorization': `Bearer ${apiKey}`
|
|
140
|
+
},
|
|
141
|
+
body: JSON.stringify(payload)
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (!response.ok) {
|
|
145
|
+
throw new Error(`API error: ${response.status}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// For now, yield the full result
|
|
149
|
+
const result = await response.json();
|
|
150
|
+
yield { delta: result.code, done: true };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get remaining credits for the user
|
|
155
|
+
*/
|
|
156
|
+
export async function getCredits(): Promise<CreditInfo> {
|
|
157
|
+
const apiKey = await getApiKey();
|
|
158
|
+
if (!apiKey) {
|
|
159
|
+
throw new Error('Not logged in. Run "vibe-weaver login" first.');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const supabaseUrl = getSupabaseUrl();
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
// Try to get profile info
|
|
166
|
+
const response = await fetch(`${supabaseUrl}/rest/v1/profiles?select=credits,plan`, {
|
|
167
|
+
headers: {
|
|
168
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
169
|
+
'apikey': `${supabaseUrl}/rest/v1/apikey`
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (response.ok) {
|
|
174
|
+
const data = await response.json();
|
|
175
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
176
|
+
return {
|
|
177
|
+
cost: 0,
|
|
178
|
+
remaining: data[0].credits || 0,
|
|
179
|
+
plan: data[0].plan || 'free'
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
// Ignore errors
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return { cost: 0, remaining: 0, plan: 'unknown' };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export default {
|
|
191
|
+
generateCode,
|
|
192
|
+
streamGenerate,
|
|
193
|
+
getCredits,
|
|
194
|
+
getCreditCost
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export type { ModelId, Platform, Language };
|
package/src/mcp/index.ts
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Client for Vibe-Weaver CLI
|
|
3
|
+
* Connects to MCP servers via JSON-RPC
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { getSupabaseUrl } from '../config/index.js';
|
|
8
|
+
|
|
9
|
+
export interface MCPServer {
|
|
10
|
+
name: string;
|
|
11
|
+
url: string;
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface MCPTool {
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
inputSchema: Record<string, any>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface MCPInitializeResult {
|
|
22
|
+
protocolVersion: string;
|
|
23
|
+
serverInfo: {
|
|
24
|
+
name: string;
|
|
25
|
+
version: string;
|
|
26
|
+
};
|
|
27
|
+
capabilities: Record<string, any>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface MCPToolsResult {
|
|
31
|
+
tools: MCPTool[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Default MCP servers provided by Vibe-Weaver
|
|
36
|
+
*/
|
|
37
|
+
export function getDefaultMCPServers(): MCPServer[] {
|
|
38
|
+
const supabaseUrl = getSupabaseUrl();
|
|
39
|
+
return [
|
|
40
|
+
{ name: 'github-mcp', url: `${supabaseUrl}/functions/v1/github-mcp`, enabled: true },
|
|
41
|
+
{ name: 'filesystem-mcp', url: `${supabaseUrl}/functions/v1/filesystem-mcp`, enabled: true },
|
|
42
|
+
{ name: 'supabase-mcp', url: `${supabaseUrl}/functions/v1/supabase-mcp`, enabled: true },
|
|
43
|
+
{ name: 'roblox-mcp', url: `${supabaseUrl}/functions/v1/roblox-mcp`, enabled: true },
|
|
44
|
+
{ name: 'linear-mcp', url: `${supabaseUrl}/functions/v1/linear-mcp`, enabled: false },
|
|
45
|
+
{ name: 'notion-mcp', url: `${supabaseUrl}/functions/v1/notion-mcp`, enabled: false },
|
|
46
|
+
{ name: 'slack-mcp', url: `${supabaseUrl}/functions/v1/slack-mcp`, enabled: false },
|
|
47
|
+
{ name: 'discord-mcp', url: `${supabaseUrl}/functions/v1/discord-mcp`, enabled: false },
|
|
48
|
+
{ name: 'vercel-mcp', url: `${supabaseUrl}/functions/v1/vercel-mcp`, enabled: false },
|
|
49
|
+
{ name: 'stripe-mcp', url: `${supabaseUrl}/functions/v1/stripe-mcp`, enabled: false },
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* MCP JSON-RPC client
|
|
55
|
+
*/
|
|
56
|
+
export class MCPClient {
|
|
57
|
+
private url: string;
|
|
58
|
+
private initialized: boolean = false;
|
|
59
|
+
private serverInfo: MCPInitializeResult['serverInfo'] | null = null;
|
|
60
|
+
private capabilities: Record<string, any> = {};
|
|
61
|
+
private requestId: number = 0;
|
|
62
|
+
|
|
63
|
+
constructor(url: string) {
|
|
64
|
+
this.url = url;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Send a JSON-RPC request
|
|
69
|
+
*/
|
|
70
|
+
private async request(method: string, params?: Record<string, any>): Promise<any> {
|
|
71
|
+
const id = ++this.requestId;
|
|
72
|
+
|
|
73
|
+
const response = await fetch(this.url, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: {
|
|
76
|
+
'Content-Type': 'application/json',
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify({
|
|
79
|
+
jsonrpc: '2.0',
|
|
80
|
+
id,
|
|
81
|
+
method,
|
|
82
|
+
params: params || {}
|
|
83
|
+
})
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
throw new Error(`MCP request failed: ${response.status} ${response.statusText}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const result = await response.json();
|
|
91
|
+
|
|
92
|
+
if (result.error) {
|
|
93
|
+
throw new Error(`MCP error: ${result.error.message}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result.result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Initialize the MCP connection
|
|
101
|
+
*/
|
|
102
|
+
async initialize(): Promise<MCPInitializeResult> {
|
|
103
|
+
try {
|
|
104
|
+
const result = await this.request('initialize', {
|
|
105
|
+
protocolVersion: '2024-11-05',
|
|
106
|
+
capabilities: {},
|
|
107
|
+
clientInfo: {
|
|
108
|
+
name: 'vibe-weaver-cli',
|
|
109
|
+
version: '1.0.0'
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
this.initialized = true;
|
|
114
|
+
this.serverInfo = result.serverInfo;
|
|
115
|
+
this.capabilities = result.capabilities;
|
|
116
|
+
|
|
117
|
+
return result;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
throw new Error(`Failed to initialize MCP server: ${error instanceof Error ? error.message : error}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* List available tools
|
|
125
|
+
*/
|
|
126
|
+
async listTools(): Promise<MCPToolsResult> {
|
|
127
|
+
if (!this.initialized) {
|
|
128
|
+
await this.initialize();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return this.request('tools/list');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Call a tool
|
|
136
|
+
*/
|
|
137
|
+
async callTool(name: string, args: Record<string, any>): Promise<any> {
|
|
138
|
+
if (!this.initialized) {
|
|
139
|
+
await this.initialize();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return this.request('tools/call', {
|
|
143
|
+
name,
|
|
144
|
+
arguments: args
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Check if server is alive
|
|
150
|
+
*/
|
|
151
|
+
async ping(): Promise<boolean> {
|
|
152
|
+
try {
|
|
153
|
+
await this.request('ping');
|
|
154
|
+
return true;
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
getServerInfo(): MCPInitializeResult['serverInfo'] | null {
|
|
161
|
+
return this.serverInfo;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
isInitialized(): boolean {
|
|
165
|
+
return this.initialized;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* MCP Manager - handles multiple MCP servers
|
|
171
|
+
*/
|
|
172
|
+
export class MCPManager {
|
|
173
|
+
private clients: Map<string, MCPClient> = new Map();
|
|
174
|
+
private servers: MCPServer[] = [];
|
|
175
|
+
|
|
176
|
+
constructor(servers?: MCPServer[]) {
|
|
177
|
+
this.servers = servers || getDefaultMCPServers();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Initialize all enabled MCP servers
|
|
182
|
+
*/
|
|
183
|
+
async initializeAll(): Promise<{ success: string[]; failed: { name: string; error: string }[] }> {
|
|
184
|
+
const success: string[] = [];
|
|
185
|
+
const failed: { name: string; error: string }[] = [];
|
|
186
|
+
|
|
187
|
+
for (const server of this.servers) {
|
|
188
|
+
if (!server.enabled) continue;
|
|
189
|
+
|
|
190
|
+
const client = new MCPClient(server.url);
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const result = await client.initialize();
|
|
194
|
+
this.clients.set(server.name, client);
|
|
195
|
+
success.push(`${server.name} (${result.serverInfo.version})`);
|
|
196
|
+
console.log(chalk.green(`✓ Connected to ${server.name}`));
|
|
197
|
+
} catch (error) {
|
|
198
|
+
failed.push({
|
|
199
|
+
name: server.name,
|
|
200
|
+
error: error instanceof Error ? error.message : String(error)
|
|
201
|
+
});
|
|
202
|
+
console.log(chalk.red(`✗ Failed to connect to ${server.name}`));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { success, failed };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* List all available tools from connected servers
|
|
211
|
+
*/
|
|
212
|
+
async listAllTools(): Promise<{ server: string; tools: MCPTool[] }[]> {
|
|
213
|
+
const results: { server: string; tools: MCPTool[] }[] = [];
|
|
214
|
+
|
|
215
|
+
for (const [name, client] of this.clients) {
|
|
216
|
+
try {
|
|
217
|
+
const result = await client.listTools();
|
|
218
|
+
results.push({ server: name, tools: result.tools });
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.warn(chalk.yellow(`Warning: Failed to list tools from ${name}: ${error}`));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return results;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Call a tool on a specific server
|
|
229
|
+
*/
|
|
230
|
+
async callTool(serverName: string, toolName: string, args: Record<string, any>): Promise<any> {
|
|
231
|
+
const client = this.clients.get(serverName);
|
|
232
|
+
|
|
233
|
+
if (!client) {
|
|
234
|
+
throw new Error(`MCP server "${serverName}" not connected`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return client.callTool(toolName, args);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get status of all servers
|
|
242
|
+
*/
|
|
243
|
+
getStatus(): { name: string; connected: boolean; tools: number }[] {
|
|
244
|
+
const status: { name: string; connected: boolean; tools: number }[] = [];
|
|
245
|
+
|
|
246
|
+
for (const server of this.servers) {
|
|
247
|
+
const client = this.clients.get(server.name);
|
|
248
|
+
status.push({
|
|
249
|
+
name: server.name,
|
|
250
|
+
connected: !!client && client.isInitialized(),
|
|
251
|
+
tools: 0 // Would need to cache tool list
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return status;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Enable/disable a server
|
|
260
|
+
*/
|
|
261
|
+
setServerEnabled(name: string, enabled: boolean): void {
|
|
262
|
+
const server = this.servers.find(s => s.name === name);
|
|
263
|
+
if (server) {
|
|
264
|
+
server.enabled = enabled;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export default {
|
|
270
|
+
MCPClient,
|
|
271
|
+
MCPManager,
|
|
272
|
+
getDefaultMCPServers
|
|
273
|
+
};
|