cloudflare-mcp-smart-proxy 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.
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Local Tool Executor - 本地工具执行器
3
+ */
4
+
5
+ import { FileOperations } from './tools/file-operations.js';
6
+ import { EditFile } from './tools/edit-file.js';
7
+ import { CommandExecutor } from './tools/command-executor.js';
8
+
9
+ export class LocalToolExecutor {
10
+ constructor(workspaceRoot) {
11
+ this.workspaceRoot = workspaceRoot;
12
+ }
13
+
14
+ /**
15
+ * 执行本地工具
16
+ */
17
+ async execute(toolName, params) {
18
+ switch (toolName) {
19
+ case 'read_file':
20
+ return await FileOperations.readFile(params, this.workspaceRoot);
21
+
22
+ case 'write_file':
23
+ case 'create_file':
24
+ return await FileOperations.writeFile(params, this.workspaceRoot);
25
+
26
+ case 'edit_file':
27
+ case 'replace_lines':
28
+ return await EditFile.execute(params, this.workspaceRoot);
29
+
30
+ case 'list_dir':
31
+ case 'list_files':
32
+ return await FileOperations.listDir(params, this.workspaceRoot);
33
+
34
+ case 'delete_file':
35
+ return await FileOperations.deleteFile(params, this.workspaceRoot);
36
+
37
+ case 'execute_command':
38
+ case 'execute_shell_command':
39
+ return await CommandExecutor.execute(params, this.workspaceRoot);
40
+
41
+ default:
42
+ throw new Error(`Unknown local tool: ${toolName}`);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * 列出所有本地工具
48
+ */
49
+ listTools() {
50
+ return [
51
+ {
52
+ name: 'read_file',
53
+ description: 'Read a file from local filesystem',
54
+ inputSchema: {
55
+ type: 'object',
56
+ properties: {
57
+ target_file: {
58
+ type: 'string',
59
+ description: 'Path to the file to read (relative to workspace root)'
60
+ },
61
+ offset: {
62
+ type: 'number',
63
+ description: 'Starting line number (0-based)'
64
+ },
65
+ limit: {
66
+ type: 'number',
67
+ description: 'Maximum number of lines to read'
68
+ }
69
+ },
70
+ required: ['target_file']
71
+ }
72
+ },
73
+ {
74
+ name: 'write_file',
75
+ description: 'Write content to a file in local filesystem',
76
+ inputSchema: {
77
+ type: 'object',
78
+ properties: {
79
+ file_path: {
80
+ type: 'string',
81
+ description: 'Path to the file to write (relative to workspace root)'
82
+ },
83
+ contents: {
84
+ type: 'string',
85
+ description: 'File contents to write'
86
+ },
87
+ overwrite: {
88
+ type: 'boolean',
89
+ description: 'Whether to overwrite existing file (default: true)'
90
+ }
91
+ },
92
+ required: ['file_path', 'contents']
93
+ }
94
+ },
95
+ {
96
+ name: 'edit_file',
97
+ description: 'Edit a file using diff-based edits. Supports multiple edits in a single operation.',
98
+ inputSchema: {
99
+ type: 'object',
100
+ properties: {
101
+ path: {
102
+ type: 'string',
103
+ description: 'Path to the file to edit (relative to workspace root)'
104
+ },
105
+ edits: {
106
+ type: 'array',
107
+ description: 'Array of edit operations',
108
+ items: {
109
+ type: 'object',
110
+ properties: {
111
+ type: {
112
+ type: 'string',
113
+ enum: ['replace', 'insert', 'delete'],
114
+ description: 'Type of edit operation'
115
+ },
116
+ startLine: {
117
+ type: 'number',
118
+ description: 'Start line number (1-based, inclusive)'
119
+ },
120
+ endLine: {
121
+ type: 'number',
122
+ description: 'End line number (1-based, inclusive)'
123
+ },
124
+ oldText: {
125
+ type: 'string',
126
+ description: 'Original text to replace (for validation)'
127
+ },
128
+ newText: {
129
+ type: 'string',
130
+ description: 'New text to insert'
131
+ }
132
+ },
133
+ required: ['type', 'startLine', 'endLine']
134
+ }
135
+ },
136
+ originalContent: {
137
+ type: 'string',
138
+ description: 'Original file content (for concurrency validation)'
139
+ }
140
+ },
141
+ required: ['path', 'edits']
142
+ }
143
+ },
144
+ {
145
+ name: 'list_dir',
146
+ description: 'List directory contents in local filesystem',
147
+ inputSchema: {
148
+ type: 'object',
149
+ properties: {
150
+ target_directory: {
151
+ type: 'string',
152
+ description: 'Path to the directory (relative to workspace root)'
153
+ },
154
+ recursive: {
155
+ type: 'boolean',
156
+ description: 'Whether to list recursively (default: false)'
157
+ }
158
+ },
159
+ required: ['target_directory']
160
+ }
161
+ },
162
+ {
163
+ name: 'delete_file',
164
+ description: 'Delete a file from local filesystem',
165
+ inputSchema: {
166
+ type: 'object',
167
+ properties: {
168
+ file_path: {
169
+ type: 'string',
170
+ description: 'Path to the file to delete (relative to workspace root)'
171
+ }
172
+ },
173
+ required: ['file_path']
174
+ }
175
+ },
176
+ {
177
+ name: 'execute_command',
178
+ description: 'Execute a shell command locally',
179
+ inputSchema: {
180
+ type: 'object',
181
+ properties: {
182
+ command: {
183
+ type: 'string',
184
+ description: 'Shell command to execute'
185
+ },
186
+ cwd: {
187
+ type: 'string',
188
+ description: 'Working directory (default: workspace root)'
189
+ }
190
+ },
191
+ required: ['command']
192
+ }
193
+ }
194
+ ];
195
+ }
196
+ }
197
+
package/src/router.js ADDED
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Smart Router - 智能路由工具请求到云端或本地
3
+ */
4
+
5
+ export class SmartRouter {
6
+ constructor(cloudUrl, cloudApiKey, localTools) {
7
+ this.cloudUrl = cloudUrl.replace(/\/$/, ''); // 移除尾部斜杠
8
+ this.cloudApiKey = cloudApiKey;
9
+ this.localTools = localTools;
10
+
11
+ // 工具路由规则
12
+ this.routingRules = {
13
+ // 本地工具(需要文件系统访问)
14
+ local: [
15
+ 'read_file',
16
+ 'write_file',
17
+ 'edit_file',
18
+ 'list_dir',
19
+ 'list_files',
20
+ 'create_file',
21
+ 'delete_file',
22
+ 'search_files',
23
+ 'get_diagnostics',
24
+ 'search_symbols',
25
+ 'get_symbol_definition',
26
+ 'get_document_symbols',
27
+ 'replace_lines',
28
+ 'execute_command',
29
+ 'execute_shell_command'
30
+ ],
31
+
32
+ // 云端工具(需要网络或云端资源)
33
+ cloud: [
34
+ 'web_search',
35
+ 'fetch_content',
36
+ 'github_search_repos',
37
+ 'github_get_repo',
38
+ 'github_get_file',
39
+ 'github_list_files',
40
+ 'github_create_issue',
41
+ 'github_list_issues',
42
+ 'github_create_pr',
43
+ 'github_get_commits',
44
+ 'resolve_library_id',
45
+ 'get_library_docs',
46
+ 'create_entities',
47
+ 'create_relations',
48
+ 'add_observations',
49
+ 'delete_entities',
50
+ 'delete_observations',
51
+ 'delete_relations',
52
+ 'search_nodes',
53
+ 'open_nodes',
54
+ 'read_graph',
55
+ 'get_datetime',
56
+ 'sequentialthinking',
57
+ 'get_config',
58
+ 'set_config',
59
+ 'codebase_search' // R2 存储的代码库搜索
60
+ ]
61
+ };
62
+ }
63
+
64
+ /**
65
+ * 判断工具应该路由到哪里
66
+ */
67
+ routeTool(toolName) {
68
+ // 检查是否是本地工具(精确匹配或前缀匹配)
69
+ for (const pattern of this.routingRules.local) {
70
+ if (toolName === pattern || toolName.startsWith(pattern + '_')) {
71
+ return 'local';
72
+ }
73
+ }
74
+
75
+ // 检查是否是云端工具
76
+ for (const pattern of this.routingRules.cloud) {
77
+ if (toolName === pattern || toolName.startsWith(pattern + '_')) {
78
+ return 'cloud';
79
+ }
80
+ }
81
+
82
+ // 默认路由到云端(安全策略)
83
+ return 'cloud';
84
+ }
85
+
86
+ /**
87
+ * 执行工具调用
88
+ */
89
+ async executeTool(toolName, params) {
90
+ const route = this.routeTool(toolName);
91
+
92
+ if (route === 'local') {
93
+ return await this.localTools.execute(toolName, params);
94
+ } else {
95
+ return await this.callCloudTool(toolName, params);
96
+ }
97
+ }
98
+
99
+ /**
100
+ * 调用云端工具
101
+ */
102
+ async callCloudTool(toolName, params) {
103
+ try {
104
+ const response = await fetch(`${this.cloudUrl}/mcp`, {
105
+ method: 'POST',
106
+ headers: {
107
+ 'Authorization': `Bearer ${this.cloudApiKey}`,
108
+ 'Content-Type': 'application/json'
109
+ },
110
+ body: JSON.stringify({
111
+ jsonrpc: '2.0',
112
+ id: Date.now(),
113
+ method: 'tools/call',
114
+ params: {
115
+ name: toolName,
116
+ arguments: params
117
+ }
118
+ })
119
+ });
120
+
121
+ if (!response.ok) {
122
+ throw new Error(`Cloud request failed: ${response.status} ${response.statusText}`);
123
+ }
124
+
125
+ const result = await response.json();
126
+
127
+ if (result.error) {
128
+ throw new Error(result.error.message || 'Cloud tool execution failed');
129
+ }
130
+
131
+ // 提取结果内容
132
+ if (result.result && result.result.content) {
133
+ // MCP 标准格式
134
+ const content = result.result.content;
135
+ if (Array.isArray(content) && content.length > 0) {
136
+ return content[0].text;
137
+ }
138
+ return content;
139
+ } else if (result.result) {
140
+ return result.result;
141
+ } else {
142
+ return result;
143
+ }
144
+ } catch (error) {
145
+ throw new Error(`Cloud tool call failed: ${error.message}`);
146
+ }
147
+ }
148
+
149
+ /**
150
+ * 获取所有工具列表(合并云端和本地)
151
+ */
152
+ async getAllTools() {
153
+ try {
154
+ const [cloudTools, localTools] = await Promise.all([
155
+ this.getCloudTools(),
156
+ this.localTools.listTools()
157
+ ]);
158
+
159
+ // 合并工具列表,添加 source 标识
160
+ const allTools = [
161
+ ...cloudTools.map(t => ({ ...t, source: 'cloud' })),
162
+ ...localTools.map(t => ({ ...t, source: 'local' }))
163
+ ];
164
+
165
+ return allTools;
166
+ } catch (error) {
167
+ console.error('Error getting all tools:', error);
168
+ // 如果云端获取失败,至少返回本地工具
169
+ return this.localTools.listTools().map(t => ({ ...t, source: 'local' }));
170
+ }
171
+ }
172
+
173
+ /**
174
+ * 获取云端工具列表
175
+ */
176
+ async getCloudTools() {
177
+ try {
178
+ const response = await fetch(`${this.cloudUrl}/mcp`, {
179
+ method: 'POST',
180
+ headers: {
181
+ 'Authorization': `Bearer ${this.cloudApiKey}`,
182
+ 'Content-Type': 'application/json'
183
+ },
184
+ body: JSON.stringify({
185
+ jsonrpc: '2.0',
186
+ id: Date.now(),
187
+ method: 'tools/list',
188
+ params: {}
189
+ })
190
+ });
191
+
192
+ if (!response.ok) {
193
+ throw new Error(`Failed to fetch cloud tools: ${response.status}`);
194
+ }
195
+
196
+ const result = await response.json();
197
+
198
+ if (result.error) {
199
+ throw new Error(result.error.message || 'Failed to fetch cloud tools');
200
+ }
201
+
202
+ return result.result?.tools || [];
203
+ } catch (error) {
204
+ console.error('Error fetching cloud tools:', error);
205
+ return [];
206
+ }
207
+ }
208
+ }
209
+
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Command Executor - 本地命令执行工具
3
+ */
4
+
5
+ import { spawn } from 'child_process';
6
+ import path from 'path';
7
+
8
+ export class CommandExecutor {
9
+ /**
10
+ * 执行命令
11
+ */
12
+ static async execute(params, workspaceRoot) {
13
+ const { command, cwd } = params;
14
+
15
+ if (!command) {
16
+ throw new Error('command parameter is required');
17
+ }
18
+
19
+ const workingDir = cwd ? path.resolve(workspaceRoot, cwd) : workspaceRoot;
20
+
21
+ return new Promise((resolve, reject) => {
22
+ // 解析命令和参数
23
+ const parts = command.trim().split(/\s+/);
24
+ const cmd = parts[0];
25
+ const args = parts.slice(1);
26
+
27
+ // 安全限制:禁止某些危险命令
28
+ const dangerousCommands = ['rm', 'del', 'format', 'shutdown', 'reboot'];
29
+ if (dangerousCommands.some(danger => cmd.includes(danger))) {
30
+ reject(new Error(`Command "${cmd}" is not allowed for security reasons`));
31
+ return;
32
+ }
33
+
34
+ const startTime = Date.now();
35
+ let stdout = '';
36
+ let stderr = '';
37
+
38
+ // 执行命令
39
+ const proc = spawn(cmd, args, {
40
+ cwd: workingDir,
41
+ shell: true,
42
+ stdio: ['ignore', 'pipe', 'pipe']
43
+ });
44
+
45
+ // 收集输出
46
+ proc.stdout.on('data', (data) => {
47
+ stdout += data.toString();
48
+ });
49
+
50
+ proc.stderr.on('data', (data) => {
51
+ stderr += data.toString();
52
+ });
53
+
54
+ // 处理完成
55
+ proc.on('close', (code) => {
56
+ const duration = Date.now() - startTime;
57
+
58
+ resolve({
59
+ success: code === 0,
60
+ exitCode: code,
61
+ stdout: stdout.trim(),
62
+ stderr: stderr.trim(),
63
+ duration: duration,
64
+ command: command,
65
+ cwd: workingDir
66
+ });
67
+ });
68
+
69
+ // 处理错误
70
+ proc.on('error', (error) => {
71
+ reject(new Error(`Command execution failed: ${error.message}`));
72
+ });
73
+
74
+ // 超时保护(30秒)
75
+ setTimeout(() => {
76
+ proc.kill();
77
+ reject(new Error('Command execution timeout (30s)'));
78
+ }, 30000);
79
+ });
80
+ }
81
+ }
82
+
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Edit File - 差异文件编辑工具
3
+ */
4
+
5
+ import fs from 'fs/promises';
6
+ import path from 'path';
7
+ import { FileOperations } from './file-operations.js';
8
+
9
+ export class EditFile {
10
+ /**
11
+ * 执行差异编辑
12
+ */
13
+ static async execute(params, workspaceRoot) {
14
+ const { path: filePath, edits, originalContent } = params;
15
+
16
+ if (!filePath || !edits || !Array.isArray(edits) || edits.length === 0) {
17
+ throw new Error('path and edits (array) parameters are required');
18
+ }
19
+
20
+ // 读取当前文件
21
+ let currentContent;
22
+ try {
23
+ currentContent = await FileOperations.readFile({
24
+ target_file: filePath
25
+ }, workspaceRoot);
26
+ } catch (error) {
27
+ throw new Error(`Failed to read file: ${error.message}`);
28
+ }
29
+
30
+ const fullContent = currentContent.content;
31
+
32
+ // 验证原始内容(防止并发修改)
33
+ if (originalContent && fullContent !== originalContent) {
34
+ throw new Error('File has been modified. Please refresh and try again.');
35
+ }
36
+
37
+ // 应用所有编辑操作
38
+ let lines = fullContent.split('\n');
39
+ const appliedEdits = [];
40
+
41
+ // 按行号排序编辑(从后往前,避免行号偏移问题)
42
+ const sortedEdits = [...edits].sort((a, b) => b.startLine - a.startLine);
43
+
44
+ for (const edit of sortedEdits) {
45
+ try {
46
+ const result = this.applyEdit(lines, edit);
47
+ lines = result.newLines;
48
+ appliedEdits.push({
49
+ ...result,
50
+ edit: edit
51
+ });
52
+ } catch (error) {
53
+ throw new Error(`Edit failed at lines ${edit.startLine}-${edit.endLine}: ${error.message}`);
54
+ }
55
+ }
56
+
57
+ // 写回文件
58
+ const newContent = lines.join('\n');
59
+ await FileOperations.writeFile({
60
+ file_path: filePath,
61
+ contents: newContent,
62
+ overwrite: true
63
+ }, workspaceRoot);
64
+
65
+ // 生成 diff
66
+ const diff = this.generateDiff(fullContent, newContent);
67
+
68
+ return {
69
+ success: true,
70
+ file: filePath,
71
+ editsApplied: appliedEdits.length,
72
+ diff: diff,
73
+ preview: this.generatePreview(lines, appliedEdits)
74
+ };
75
+ }
76
+
77
+ /**
78
+ * 应用单个编辑操作
79
+ */
80
+ static applyEdit(lines, edit) {
81
+ const { type, startLine, endLine, oldText, newText } = edit;
82
+
83
+ // 验证行号(1-based to 0-based)
84
+ const start = startLine - 1;
85
+ const end = endLine - 1;
86
+
87
+ if (start < 0 || end >= lines.length || start > end) {
88
+ throw new Error(`Invalid line range: ${startLine}-${endLine} (file has ${lines.length} lines)`);
89
+ }
90
+
91
+ // 提取原始文本(用于验证)
92
+ const actualOldText = lines.slice(start, end + 1).join('\n');
93
+
94
+ // 验证原始文本(如果提供了 oldText)
95
+ if (oldText !== undefined && actualOldText !== oldText) {
96
+ throw new Error(`Content mismatch at lines ${startLine}-${endLine}. Expected:\n${oldText}\nGot:\n${actualOldText}`);
97
+ }
98
+
99
+ // 执行替换
100
+ const newLines = newText !== undefined ? newText.split('\n') : [];
101
+
102
+ const result = {
103
+ startLine: start,
104
+ endLine: end,
105
+ newLines: [
106
+ ...lines.slice(0, start),
107
+ ...newLines,
108
+ ...lines.slice(end + 1)
109
+ ]
110
+ };
111
+
112
+ return result;
113
+ }
114
+
115
+ /**
116
+ * 生成简单的行级 diff
117
+ */
118
+ static generateDiff(oldContent, newContent) {
119
+ const oldLines = oldContent.split('\n');
120
+ const newLines = newContent.split('\n');
121
+ const diff = [];
122
+
123
+ let i = 0, j = 0;
124
+ while (i < oldLines.length || j < newLines.length) {
125
+ if (i >= oldLines.length) {
126
+ // 新文件有额外行
127
+ diff.push(`+${newLines[j]}`);
128
+ j++;
129
+ } else if (j >= newLines.length) {
130
+ // 旧文件有删除行
131
+ diff.push(`-${oldLines[i]}`);
132
+ i++;
133
+ } else if (oldLines[i] === newLines[j]) {
134
+ // 相同行
135
+ diff.push(` ${oldLines[i]}`);
136
+ i++;
137
+ j++;
138
+ } else {
139
+ // 查找下一个匹配
140
+ const nextMatch = this.findNextMatch(oldLines, newLines, i, j);
141
+ if (nextMatch.oldIndex === i && nextMatch.newIndex === j) {
142
+ // 没有匹配,都是修改
143
+ diff.push(`-${oldLines[i]}`);
144
+ diff.push(`+${newLines[j]}`);
145
+ i++;
146
+ j++;
147
+ } else {
148
+ // 处理中间的差异
149
+ while (i < nextMatch.oldIndex) {
150
+ diff.push(`-${oldLines[i]}`);
151
+ i++;
152
+ }
153
+ while (j < nextMatch.newIndex) {
154
+ diff.push(`+${newLines[j]}`);
155
+ j++;
156
+ }
157
+ }
158
+ }
159
+ }
160
+
161
+ return diff.join('\n');
162
+ }
163
+
164
+ /**
165
+ * 查找下一个匹配的行
166
+ */
167
+ static findNextMatch(oldLines, newLines, oldStart, newStart) {
168
+ // 简单的贪心匹配:查找最近的匹配行
169
+ for (let i = oldStart + 1; i < oldLines.length && i < oldStart + 10; i++) {
170
+ for (let j = newStart + 1; j < newLines.length && j < newStart + 10; j++) {
171
+ if (oldLines[i] === newLines[j]) {
172
+ return { oldIndex: i, newIndex: j };
173
+ }
174
+ }
175
+ }
176
+ return { oldIndex: oldStart, newIndex: newStart };
177
+ }
178
+
179
+ /**
180
+ * 生成编辑预览
181
+ */
182
+ static generatePreview(lines, appliedEdits) {
183
+ const previews = [];
184
+
185
+ for (const applied of appliedEdits) {
186
+ const edit = applied.edit;
187
+ const startLine = applied.startLine;
188
+ const endLine = applied.endLine;
189
+ const newLinesCount = applied.newLines.length - (endLine - startLine + 1);
190
+
191
+ const contextStart = Math.max(0, startLine - 2);
192
+ const contextEnd = Math.min(lines.length, startLine + newLinesCount + 2);
193
+
194
+ previews.push({
195
+ range: `${edit.startLine}-${edit.endLine}`,
196
+ context: lines.slice(contextStart, contextEnd).join('\n'),
197
+ changes: applied.newLines.slice(startLine, startLine + newLinesCount).join('\n')
198
+ });
199
+ }
200
+
201
+ return previews;
202
+ }
203
+ }
204
+