@agents-at-scale/ark 0.1.35 → 0.1.36-rc1
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/dist/arkServices.d.ts +42 -0
- package/dist/arkServices.js +138 -0
- package/dist/arkServices.spec.d.ts +1 -0
- package/dist/arkServices.spec.js +24 -0
- package/dist/charts/charts.d.ts +5 -0
- package/dist/charts/charts.js +6 -0
- package/dist/charts/dependencies.d.ts +6 -0
- package/dist/charts/dependencies.js +50 -0
- package/dist/charts/types.d.ts +40 -0
- package/dist/charts/types.js +1 -0
- package/dist/commands/agents/index.d.ts +3 -0
- package/dist/commands/agents/index.js +65 -0
- package/dist/commands/agents/index.spec.d.ts +1 -0
- package/dist/commands/agents/index.spec.js +67 -0
- package/dist/commands/agents/selector.d.ts +8 -0
- package/dist/commands/agents/selector.js +53 -0
- package/dist/commands/agents.d.ts +2 -0
- package/dist/commands/agents.js +53 -0
- package/dist/commands/chat/index.d.ts +3 -0
- package/dist/commands/chat/index.js +29 -0
- package/dist/commands/chat.d.ts +2 -0
- package/dist/commands/chat.js +45 -0
- package/dist/commands/cluster/get.d.ts +2 -0
- package/dist/commands/cluster/get.js +39 -0
- package/dist/commands/cluster/get.spec.d.ts +1 -0
- package/dist/commands/cluster/get.spec.js +92 -0
- package/dist/commands/cluster/index.d.ts +2 -1
- package/dist/commands/cluster/index.js +3 -5
- package/dist/commands/cluster/index.spec.d.ts +1 -0
- package/dist/commands/cluster/index.spec.js +24 -0
- package/dist/commands/completion/index.d.ts +3 -0
- package/dist/commands/completion/index.js +230 -0
- package/dist/commands/completion/index.spec.d.ts +1 -0
- package/dist/commands/completion/index.spec.js +34 -0
- package/dist/commands/completion.js +159 -2
- package/dist/commands/config/index.d.ts +3 -0
- package/dist/commands/config/index.js +42 -0
- package/dist/commands/config/index.spec.d.ts +1 -0
- package/dist/commands/config/index.spec.js +78 -0
- package/dist/commands/config.d.ts +0 -3
- package/dist/commands/config.js +38 -321
- package/dist/commands/dashboard/index.d.ts +4 -0
- package/dist/commands/dashboard/index.js +39 -0
- package/dist/commands/dashboard.d.ts +3 -0
- package/dist/commands/dashboard.js +39 -0
- package/dist/commands/dev/index.d.ts +3 -0
- package/dist/commands/dev/index.js +9 -0
- package/dist/commands/dev/tool/check.d.ts +2 -0
- package/dist/commands/dev/tool/check.js +142 -0
- package/dist/commands/dev/tool/clean.d.ts +2 -0
- package/dist/commands/dev/tool/clean.js +153 -0
- package/dist/commands/dev/tool/generate.d.ts +2 -0
- package/dist/commands/dev/tool/generate.js +28 -0
- package/dist/commands/dev/tool/index.d.ts +2 -0
- package/dist/commands/dev/tool/index.js +14 -0
- package/dist/commands/dev/tool/init.d.ts +2 -0
- package/dist/commands/dev/tool/init.js +320 -0
- package/dist/commands/dev/tool/shared.d.ts +5 -0
- package/dist/commands/dev/tool/shared.js +258 -0
- package/dist/commands/dev/tool/status.d.ts +2 -0
- package/dist/commands/dev/tool/status.js +136 -0
- package/dist/commands/dev/tool-generate.spec.d.ts +1 -0
- package/dist/commands/dev/tool-generate.spec.js +163 -0
- package/dist/commands/dev/tool.d.ts +2 -0
- package/dist/commands/dev/tool.js +559 -0
- package/dist/commands/dev/tool.spec.d.ts +1 -0
- package/dist/commands/dev/tool.spec.js +48 -0
- package/dist/commands/docs/index.d.ts +4 -0
- package/dist/commands/docs/index.js +18 -0
- package/dist/commands/generate/config.js +5 -24
- package/dist/commands/generate/generators/mcpserver.d.ts +2 -1
- package/dist/commands/generate/generators/mcpserver.js +26 -5
- package/dist/commands/generate/generators/project.js +22 -41
- package/dist/commands/generate/index.d.ts +2 -1
- package/dist/commands/generate/index.js +1 -1
- package/dist/commands/install/index.d.ts +8 -0
- package/dist/commands/install/index.js +295 -0
- package/dist/commands/install/index.spec.d.ts +1 -0
- package/dist/commands/install/index.spec.js +143 -0
- package/dist/commands/install.d.ts +3 -0
- package/dist/commands/install.js +147 -0
- package/dist/commands/models/create.d.ts +1 -0
- package/dist/commands/models/create.js +213 -0
- package/dist/commands/models/create.spec.d.ts +1 -0
- package/dist/commands/models/create.spec.js +125 -0
- package/dist/commands/models/index.d.ts +3 -0
- package/dist/commands/models/index.js +75 -0
- package/dist/commands/models/index.spec.d.ts +1 -0
- package/dist/commands/models/index.spec.js +96 -0
- package/dist/commands/models/selector.d.ts +8 -0
- package/dist/commands/models/selector.js +53 -0
- package/dist/commands/query/index.d.ts +3 -0
- package/dist/commands/query/index.js +24 -0
- package/dist/commands/query/index.spec.d.ts +1 -0
- package/dist/commands/query/index.spec.js +53 -0
- package/dist/commands/routes/index.d.ts +3 -0
- package/dist/commands/routes/index.js +93 -0
- package/dist/commands/routes.d.ts +2 -0
- package/dist/commands/routes.js +101 -0
- package/dist/commands/status/index.d.ts +3 -0
- package/dist/commands/status/index.js +281 -0
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.js +33 -0
- package/dist/commands/targets/index.d.ts +3 -0
- package/dist/commands/targets/index.js +72 -0
- package/dist/commands/targets/index.spec.d.ts +1 -0
- package/dist/commands/targets/index.spec.js +154 -0
- package/dist/commands/targets.d.ts +2 -0
- package/dist/commands/targets.js +65 -0
- package/dist/commands/teams/index.d.ts +3 -0
- package/dist/commands/teams/index.js +64 -0
- package/dist/commands/teams/index.spec.d.ts +1 -0
- package/dist/commands/teams/index.spec.js +70 -0
- package/dist/commands/teams/selector.d.ts +8 -0
- package/dist/commands/teams/selector.js +55 -0
- package/dist/commands/tools/index.d.ts +3 -0
- package/dist/commands/tools/index.js +49 -0
- package/dist/commands/tools/index.spec.d.ts +1 -0
- package/dist/commands/tools/index.spec.js +70 -0
- package/dist/commands/tools/selector.d.ts +8 -0
- package/dist/commands/tools/selector.js +53 -0
- package/dist/commands/uninstall/index.d.ts +3 -0
- package/dist/commands/uninstall/index.js +101 -0
- package/dist/commands/uninstall/index.spec.d.ts +1 -0
- package/dist/commands/uninstall/index.spec.js +125 -0
- package/dist/commands/uninstall.d.ts +2 -0
- package/dist/commands/uninstall.js +83 -0
- package/dist/components/ChatUI.d.ts +16 -0
- package/dist/components/ChatUI.js +801 -0
- package/dist/components/StatusView.d.ts +10 -0
- package/dist/components/StatusView.js +39 -0
- package/dist/components/statusChecker.d.ts +14 -24
- package/dist/components/statusChecker.js +295 -129
- package/dist/config.d.ts +3 -22
- package/dist/config.js +10 -161
- package/dist/index.d.ts +1 -1
- package/dist/index.js +42 -42
- package/dist/lib/arkApiClient.d.ts +53 -0
- package/dist/lib/arkApiClient.js +102 -0
- package/dist/lib/arkApiProxy.d.ts +9 -0
- package/dist/lib/arkApiProxy.js +22 -0
- package/dist/lib/arkServiceProxy.d.ts +14 -0
- package/dist/lib/arkServiceProxy.js +95 -0
- package/dist/lib/arkStatus.d.ts +10 -0
- package/dist/lib/arkStatus.js +79 -0
- package/dist/lib/arkStatus.spec.d.ts +1 -0
- package/dist/lib/arkStatus.spec.js +49 -0
- package/dist/lib/chatClient.d.ts +33 -0
- package/dist/lib/chatClient.js +93 -0
- package/dist/lib/cluster.d.ts +2 -1
- package/dist/lib/cluster.js +37 -16
- package/dist/lib/cluster.spec.d.ts +1 -0
- package/dist/lib/cluster.spec.js +338 -0
- package/dist/lib/commandUtils.d.ts +4 -0
- package/dist/lib/commandUtils.js +18 -0
- package/dist/lib/commandUtils.test.d.ts +1 -0
- package/dist/lib/commandUtils.test.js +44 -0
- package/dist/lib/commands.d.ts +16 -0
- package/dist/lib/commands.js +29 -0
- package/dist/lib/commands.spec.d.ts +1 -0
- package/dist/lib/commands.spec.js +146 -0
- package/dist/lib/config.d.ts +26 -80
- package/dist/lib/config.js +70 -205
- package/dist/lib/config.spec.d.ts +1 -0
- package/dist/lib/config.spec.js +99 -0
- package/dist/lib/config.test.d.ts +1 -0
- package/dist/lib/config.test.js +93 -0
- package/dist/lib/consts.d.ts +0 -1
- package/dist/lib/consts.js +0 -2
- package/dist/lib/consts.spec.d.ts +1 -0
- package/dist/lib/consts.spec.js +15 -0
- package/dist/lib/dev/tools/analyzer.d.ts +30 -0
- package/dist/lib/dev/tools/analyzer.js +190 -0
- package/dist/lib/dev/tools/discover_tools.py +392 -0
- package/dist/lib/dev/tools/mcp-types.d.ts +28 -0
- package/dist/lib/dev/tools/mcp-types.js +86 -0
- package/dist/lib/dev/tools/types.d.ts +50 -0
- package/dist/lib/dev/tools/types.js +1 -0
- package/dist/lib/errors.js +1 -1
- package/dist/lib/errors.spec.d.ts +1 -0
- package/dist/lib/errors.spec.js +221 -0
- package/dist/lib/exec.d.ts +0 -4
- package/dist/lib/exec.js +0 -11
- package/dist/lib/executeQuery.d.ts +20 -0
- package/dist/lib/executeQuery.js +135 -0
- package/dist/lib/executeQuery.spec.d.ts +1 -0
- package/dist/lib/executeQuery.spec.js +170 -0
- package/dist/lib/nextSteps.d.ts +4 -0
- package/dist/lib/nextSteps.js +20 -0
- package/dist/lib/nextSteps.spec.d.ts +1 -0
- package/dist/lib/nextSteps.spec.js +59 -0
- package/dist/lib/output.d.ts +36 -0
- package/dist/lib/output.js +89 -0
- package/dist/lib/output.spec.d.ts +1 -0
- package/dist/lib/output.spec.js +123 -0
- package/dist/lib/portUtils.d.ts +8 -0
- package/dist/lib/portUtils.js +39 -0
- package/dist/lib/queryRunner.d.ts +22 -0
- package/dist/lib/queryRunner.js +142 -0
- package/dist/lib/startup.d.ts +9 -0
- package/dist/lib/startup.js +87 -0
- package/dist/lib/startup.spec.d.ts +1 -0
- package/dist/lib/startup.spec.js +152 -0
- package/dist/lib/types.d.ts +87 -3
- package/dist/lib/versions.d.ts +23 -0
- package/dist/lib/versions.js +51 -0
- package/dist/types/types.d.ts +40 -0
- package/dist/types/types.js +1 -0
- package/dist/ui/AgentSelector.d.ts +8 -0
- package/dist/ui/AgentSelector.js +53 -0
- package/dist/ui/MainMenu.d.ts +5 -1
- package/dist/ui/MainMenu.js +226 -91
- package/dist/ui/ModelSelector.d.ts +8 -0
- package/dist/ui/ModelSelector.js +53 -0
- package/dist/ui/TeamSelector.d.ts +8 -0
- package/dist/ui/TeamSelector.js +55 -0
- package/dist/ui/ToolSelector.d.ts +8 -0
- package/dist/ui/ToolSelector.js +53 -0
- package/dist/ui/statusFormatter.d.ts +22 -7
- package/dist/ui/statusFormatter.js +39 -39
- package/dist/ui/statusFormatter.spec.d.ts +1 -0
- package/dist/ui/statusFormatter.spec.js +58 -0
- package/package.json +16 -5
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
export class ArkDevToolAnalyzer {
|
|
8
|
+
constructor() {
|
|
9
|
+
// The Python script is always adjacent to this file
|
|
10
|
+
// In dev: src/lib/dev/tools/discover_tools.py
|
|
11
|
+
// In prod: dist/lib/dev/tools/discover_tools.py (copied by postbuild)
|
|
12
|
+
this.discoverToolsScript = path.join(__dirname, 'discover_tools.py');
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Analyze a tool directory and return its status
|
|
16
|
+
*/
|
|
17
|
+
async analyzeToolDirectory(toolPath) {
|
|
18
|
+
const absolutePath = path.resolve(toolPath);
|
|
19
|
+
// Check if path exists
|
|
20
|
+
if (!fs.existsSync(absolutePath)) {
|
|
21
|
+
throw new Error(`Path not found: ${absolutePath}`);
|
|
22
|
+
}
|
|
23
|
+
// Get project info
|
|
24
|
+
const projectInfo = this.getProjectInfo(absolutePath);
|
|
25
|
+
// Discover tools using Python script
|
|
26
|
+
const discovery = await this.discoverTools(absolutePath);
|
|
27
|
+
// Extract all tools from discovery
|
|
28
|
+
const tools = this.extractTools(discovery);
|
|
29
|
+
return {
|
|
30
|
+
...projectInfo,
|
|
31
|
+
discovery,
|
|
32
|
+
tools,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Get project information by checking for Python project files
|
|
37
|
+
*/
|
|
38
|
+
getProjectInfo(dirPath) {
|
|
39
|
+
const info = {
|
|
40
|
+
path: dirPath,
|
|
41
|
+
platform: 'python3',
|
|
42
|
+
projectType: 'unknown',
|
|
43
|
+
hasVenv: false,
|
|
44
|
+
fastMCP: false,
|
|
45
|
+
};
|
|
46
|
+
// Check for virtual environment
|
|
47
|
+
info.hasVenv =
|
|
48
|
+
fs.existsSync(path.join(dirPath, '.venv')) ||
|
|
49
|
+
fs.existsSync(path.join(dirPath, 'venv'));
|
|
50
|
+
// Check Python project type and FastMCP presence
|
|
51
|
+
const pyprojectPath = path.join(dirPath, 'pyproject.toml');
|
|
52
|
+
const requirementsPath = path.join(dirPath, 'requirements.txt');
|
|
53
|
+
if (fs.existsSync(pyprojectPath)) {
|
|
54
|
+
info.projectType = 'pyproject';
|
|
55
|
+
const content = fs.readFileSync(pyprojectPath, 'utf-8');
|
|
56
|
+
if (content.includes('fastmcp')) {
|
|
57
|
+
info.fastMCP = true;
|
|
58
|
+
// Try to extract version
|
|
59
|
+
const versionMatch = content.match(/fastmcp[>=<~]*([0-9.]+)/);
|
|
60
|
+
if (versionMatch) {
|
|
61
|
+
info.fastMCPVersion = versionMatch[1];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else if (fs.existsSync(requirementsPath)) {
|
|
66
|
+
info.projectType = 'requirements';
|
|
67
|
+
const content = fs.readFileSync(requirementsPath, 'utf-8');
|
|
68
|
+
if (content.includes('fastmcp')) {
|
|
69
|
+
info.fastMCP = true;
|
|
70
|
+
const versionMatch = content.match(/fastmcp[>=<~]*([0-9.]+)/);
|
|
71
|
+
if (versionMatch) {
|
|
72
|
+
info.fastMCPVersion = versionMatch[1];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return info;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Discover project configuration
|
|
80
|
+
*/
|
|
81
|
+
async discoverProject(targetPath) {
|
|
82
|
+
try {
|
|
83
|
+
// Check if Python is available
|
|
84
|
+
try {
|
|
85
|
+
execSync('python3 --version', { stdio: 'ignore' });
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
console.warn('Python 3 not found');
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
// Check if discover_tools.py exists
|
|
92
|
+
if (!fs.existsSync(this.discoverToolsScript)) {
|
|
93
|
+
console.warn(`discover_tools.py not found at ${this.discoverToolsScript}`);
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
// Run the discovery script with 'project' command
|
|
97
|
+
const result = execSync(`python3 "${this.discoverToolsScript}" project "${targetPath}"`, {
|
|
98
|
+
encoding: 'utf-8',
|
|
99
|
+
maxBuffer: 1024 * 1024, // 1MB buffer
|
|
100
|
+
});
|
|
101
|
+
return JSON.parse(result);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
console.error('Project discovery failed:', error);
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Discover tools using the Python script
|
|
110
|
+
*/
|
|
111
|
+
async discoverTools(targetPath) {
|
|
112
|
+
try {
|
|
113
|
+
// Check if Python is available
|
|
114
|
+
try {
|
|
115
|
+
execSync('python3 --version', { stdio: 'ignore' });
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
console.warn('Python 3 not found, skipping tool discovery');
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
// Check if discover_tools.py exists
|
|
122
|
+
if (!fs.existsSync(this.discoverToolsScript)) {
|
|
123
|
+
console.warn(`discover_tools.py not found at ${this.discoverToolsScript}`);
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
// Run the discovery script with 'tools' command
|
|
127
|
+
const result = execSync(`python3 "${this.discoverToolsScript}" tools "${targetPath}"`, {
|
|
128
|
+
encoding: 'utf-8',
|
|
129
|
+
maxBuffer: 1024 * 1024 * 10, // 10MB buffer
|
|
130
|
+
});
|
|
131
|
+
return JSON.parse(result);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
console.error('Tool discovery failed:', error);
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Recursively find all MCP tools in a project
|
|
140
|
+
* This is a naive implementation that searches all Python files in the project tree
|
|
141
|
+
*/
|
|
142
|
+
async findProjectTools(projectRoot) {
|
|
143
|
+
try {
|
|
144
|
+
// Check if Python is available
|
|
145
|
+
try {
|
|
146
|
+
execSync('python3 --version', { stdio: 'ignore' });
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
console.warn('Python 3 not found');
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
// Check if discover_tools.py exists
|
|
153
|
+
if (!fs.existsSync(this.discoverToolsScript)) {
|
|
154
|
+
console.warn(`discover_tools.py not found at ${this.discoverToolsScript}`);
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
// Run the discovery script with 'project-tools' command
|
|
158
|
+
const result = execSync(`python3 "${this.discoverToolsScript}" project-tools "${projectRoot}"`, {
|
|
159
|
+
encoding: 'utf-8',
|
|
160
|
+
maxBuffer: 1024 * 1024 * 10, // 10MB buffer
|
|
161
|
+
});
|
|
162
|
+
return JSON.parse(result);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
console.error('Project tools discovery failed:', error);
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Extract all tools from discovery result
|
|
171
|
+
*/
|
|
172
|
+
extractTools(discovery) {
|
|
173
|
+
if (!discovery)
|
|
174
|
+
return [];
|
|
175
|
+
// Check if it's a directory result
|
|
176
|
+
if ('files' in discovery) {
|
|
177
|
+
const dirResult = discovery;
|
|
178
|
+
const tools = [];
|
|
179
|
+
for (const file of dirResult.files) {
|
|
180
|
+
if (file.success && file.tools) {
|
|
181
|
+
tools.push(...file.tools);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return tools;
|
|
185
|
+
}
|
|
186
|
+
// Single file result
|
|
187
|
+
const fileResult = discovery;
|
|
188
|
+
return fileResult.success ? fileResult.tools : [];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Discover MCP tools in Python files using static analysis.
|
|
4
|
+
Uses only Python stdlib - no external dependencies required.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import ast
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
import os
|
|
11
|
+
from typing import Dict, List, Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MCPToolDiscoverer(ast.NodeVisitor):
|
|
15
|
+
"""AST visitor to find MCP tool definitions"""
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
self.tools = []
|
|
19
|
+
self.mcp_var_name = None
|
|
20
|
+
self.imports = {}
|
|
21
|
+
self.current_function = None # Track if we're inside a function
|
|
22
|
+
|
|
23
|
+
def visit_ImportFrom(self, node):
|
|
24
|
+
"""Track imports to identify FastMCP usage"""
|
|
25
|
+
if node.module == 'fastmcp':
|
|
26
|
+
for alias in node.names:
|
|
27
|
+
if alias.name == 'FastMCP':
|
|
28
|
+
self.imports['FastMCP'] = alias.asname or alias.name
|
|
29
|
+
self.generic_visit(node)
|
|
30
|
+
|
|
31
|
+
def visit_Assign(self, node):
|
|
32
|
+
"""Find mcp = FastMCP(...) assignments"""
|
|
33
|
+
if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
|
|
34
|
+
var_name = node.targets[0].id
|
|
35
|
+
if isinstance(node.value, ast.Call):
|
|
36
|
+
if isinstance(node.value.func, ast.Name):
|
|
37
|
+
if node.value.func.id in self.imports.values():
|
|
38
|
+
self.mcp_var_name = var_name
|
|
39
|
+
# Extract server name if provided
|
|
40
|
+
if node.value.args:
|
|
41
|
+
if isinstance(node.value.args[0], ast.Constant):
|
|
42
|
+
self.server_name = node.value.args[0].value
|
|
43
|
+
self.generic_visit(node)
|
|
44
|
+
|
|
45
|
+
def visit_FunctionDef(self, node):
|
|
46
|
+
"""Find functions decorated with @mcp.tool() - both at module level and nested"""
|
|
47
|
+
# Save the parent function context
|
|
48
|
+
parent_function = self.current_function
|
|
49
|
+
self.current_function = node.name
|
|
50
|
+
|
|
51
|
+
# Check if this function is decorated as a tool
|
|
52
|
+
for decorator in node.decorator_list:
|
|
53
|
+
is_mcp_tool = False
|
|
54
|
+
tool_config = {}
|
|
55
|
+
|
|
56
|
+
# Check for @mcp.tool() or @mcp.tool
|
|
57
|
+
if isinstance(decorator, ast.Call):
|
|
58
|
+
if isinstance(decorator.func, ast.Attribute):
|
|
59
|
+
# Check for mcp.tool where mcp is a variable
|
|
60
|
+
if (isinstance(decorator.func.value, ast.Name) and
|
|
61
|
+
decorator.func.value.id == self.mcp_var_name and
|
|
62
|
+
decorator.func.attr == 'tool'):
|
|
63
|
+
is_mcp_tool = True
|
|
64
|
+
# Extract any config from @mcp.tool(name="...", description="...")
|
|
65
|
+
for keyword in decorator.keywords:
|
|
66
|
+
if isinstance(keyword.value, ast.Constant):
|
|
67
|
+
tool_config[keyword.arg] = keyword.value.value
|
|
68
|
+
elif isinstance(decorator, ast.Attribute):
|
|
69
|
+
# Check for @mcp.tool without parentheses
|
|
70
|
+
if (isinstance(decorator.value, ast.Name) and
|
|
71
|
+
decorator.value.id == self.mcp_var_name and
|
|
72
|
+
decorator.attr == 'tool'):
|
|
73
|
+
is_mcp_tool = True
|
|
74
|
+
# Also check for @mcp.tool where mcp is a parameter (e.g., in register_tools(mcp))
|
|
75
|
+
elif (decorator.value.id if isinstance(decorator.value, ast.Name) else None) == 'mcp' and \
|
|
76
|
+
decorator.attr == 'tool':
|
|
77
|
+
is_mcp_tool = True
|
|
78
|
+
|
|
79
|
+
if is_mcp_tool:
|
|
80
|
+
tool_info = self.extract_function_info(node)
|
|
81
|
+
tool_info.update(tool_config)
|
|
82
|
+
if parent_function:
|
|
83
|
+
tool_info['registered_in'] = parent_function
|
|
84
|
+
self.tools.append(tool_info)
|
|
85
|
+
|
|
86
|
+
# Visit nested functions to find tools defined inside this function
|
|
87
|
+
self.generic_visit(node)
|
|
88
|
+
|
|
89
|
+
# Restore parent function context
|
|
90
|
+
self.current_function = parent_function
|
|
91
|
+
|
|
92
|
+
def visit_AsyncFunctionDef(self, node):
|
|
93
|
+
"""Handle async functions the same way as regular functions"""
|
|
94
|
+
# Save the parent function context
|
|
95
|
+
parent_function = self.current_function
|
|
96
|
+
self.current_function = node.name
|
|
97
|
+
|
|
98
|
+
# Check if this async function is decorated as a tool
|
|
99
|
+
for decorator in node.decorator_list:
|
|
100
|
+
is_mcp_tool = False
|
|
101
|
+
tool_config = {}
|
|
102
|
+
|
|
103
|
+
# Check for @mcp.tool() or @mcp.tool
|
|
104
|
+
if isinstance(decorator, ast.Call):
|
|
105
|
+
if isinstance(decorator.func, ast.Attribute):
|
|
106
|
+
if (isinstance(decorator.func.value, ast.Name) and
|
|
107
|
+
decorator.func.attr == 'tool'):
|
|
108
|
+
# Could be mcp.tool where mcp is the var or a parameter
|
|
109
|
+
if decorator.func.value.id == self.mcp_var_name or decorator.func.value.id == 'mcp':
|
|
110
|
+
is_mcp_tool = True
|
|
111
|
+
for keyword in decorator.keywords:
|
|
112
|
+
if isinstance(keyword.value, ast.Constant):
|
|
113
|
+
tool_config[keyword.arg] = keyword.value.value
|
|
114
|
+
elif isinstance(decorator, ast.Attribute):
|
|
115
|
+
if decorator.attr == 'tool':
|
|
116
|
+
# Could be @mcp.tool where mcp is the var or a parameter
|
|
117
|
+
if isinstance(decorator.value, ast.Name):
|
|
118
|
+
if decorator.value.id == self.mcp_var_name or decorator.value.id == 'mcp':
|
|
119
|
+
is_mcp_tool = True
|
|
120
|
+
|
|
121
|
+
if is_mcp_tool:
|
|
122
|
+
tool_info = self.extract_function_info(node)
|
|
123
|
+
tool_info.update(tool_config)
|
|
124
|
+
if parent_function:
|
|
125
|
+
tool_info['registered_in'] = parent_function
|
|
126
|
+
self.tools.append(tool_info)
|
|
127
|
+
|
|
128
|
+
# Visit nested functions
|
|
129
|
+
self.generic_visit(node)
|
|
130
|
+
|
|
131
|
+
# Restore parent function context
|
|
132
|
+
self.current_function = parent_function
|
|
133
|
+
|
|
134
|
+
def extract_function_info(self, node):
|
|
135
|
+
"""Extract function name, parameters, and docstring"""
|
|
136
|
+
info = {
|
|
137
|
+
'name': node.name,
|
|
138
|
+
'parameters': [],
|
|
139
|
+
'return_type': None,
|
|
140
|
+
'docstring': ast.get_docstring(node)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# Extract parameters
|
|
144
|
+
for arg in node.args.args:
|
|
145
|
+
param = {'name': arg.arg}
|
|
146
|
+
if arg.annotation:
|
|
147
|
+
param['type'] = ast.unparse(arg.annotation) if hasattr(ast, 'unparse') else self.unparse_annotation(arg.annotation)
|
|
148
|
+
info['parameters'].append(param)
|
|
149
|
+
|
|
150
|
+
# Extract return type
|
|
151
|
+
if node.returns:
|
|
152
|
+
info['return_type'] = ast.unparse(node.returns) if hasattr(ast, 'unparse') else self.unparse_annotation(node.returns)
|
|
153
|
+
|
|
154
|
+
return info
|
|
155
|
+
|
|
156
|
+
def unparse_annotation(self, annotation):
|
|
157
|
+
"""Fallback for Python < 3.9 without ast.unparse"""
|
|
158
|
+
if isinstance(annotation, ast.Name):
|
|
159
|
+
return annotation.id
|
|
160
|
+
elif isinstance(annotation, ast.Constant):
|
|
161
|
+
return repr(annotation.value)
|
|
162
|
+
else:
|
|
163
|
+
return 'Any'
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def discover_tools_in_file(filepath):
|
|
167
|
+
"""Discover MCP tools in a single Python file"""
|
|
168
|
+
try:
|
|
169
|
+
with open(filepath, 'r') as f:
|
|
170
|
+
content = f.read()
|
|
171
|
+
|
|
172
|
+
tree = ast.parse(content)
|
|
173
|
+
discoverer = MCPToolDiscoverer()
|
|
174
|
+
discoverer.visit(tree)
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
'success': True,
|
|
178
|
+
'file': filepath,
|
|
179
|
+
'tools': discoverer.tools,
|
|
180
|
+
'uses_fastmcp': bool(discoverer.mcp_var_name),
|
|
181
|
+
'mcp_instance': discoverer.mcp_var_name,
|
|
182
|
+
'server_name': getattr(discoverer, 'server_name', None)
|
|
183
|
+
}
|
|
184
|
+
except SyntaxError as e:
|
|
185
|
+
return {
|
|
186
|
+
'success': False,
|
|
187
|
+
'file': filepath,
|
|
188
|
+
'error': f'Syntax error: {str(e)}',
|
|
189
|
+
'tools': []
|
|
190
|
+
}
|
|
191
|
+
except Exception as e:
|
|
192
|
+
return {
|
|
193
|
+
'success': False,
|
|
194
|
+
'file': filepath,
|
|
195
|
+
'error': str(e),
|
|
196
|
+
'tools': []
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def discover_tools_in_directory(dirpath):
|
|
201
|
+
"""Discover MCP tools in Python files in root directory only (no recursion)"""
|
|
202
|
+
results = {
|
|
203
|
+
'directory': dirpath,
|
|
204
|
+
'files': [],
|
|
205
|
+
'total_tools': 0,
|
|
206
|
+
'uses_fastmcp': False
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
# Only check Python files in the root directory
|
|
210
|
+
for file in os.listdir(dirpath):
|
|
211
|
+
if file.endswith('.py'):
|
|
212
|
+
filepath = os.path.join(dirpath, file)
|
|
213
|
+
if os.path.isfile(filepath):
|
|
214
|
+
file_result = discover_tools_in_file(filepath)
|
|
215
|
+
results['files'].append(file_result)
|
|
216
|
+
results['total_tools'] += len(file_result.get('tools', []))
|
|
217
|
+
if file_result.get('uses_fastmcp'):
|
|
218
|
+
results['uses_fastmcp'] = True
|
|
219
|
+
|
|
220
|
+
return results
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def discover_project(dirpath):
|
|
224
|
+
"""Discover project configuration and type"""
|
|
225
|
+
result = {
|
|
226
|
+
'path': dirpath,
|
|
227
|
+
'exists': os.path.exists(dirpath),
|
|
228
|
+
'is_directory': os.path.isdir(dirpath) if os.path.exists(dirpath) else False,
|
|
229
|
+
'platform': None,
|
|
230
|
+
'project_type': None,
|
|
231
|
+
'project_file': None,
|
|
232
|
+
'project_name': None,
|
|
233
|
+
'project_version': None,
|
|
234
|
+
'has_fastmcp': False,
|
|
235
|
+
'fastmcp_version': None
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if not result['exists']:
|
|
239
|
+
return result
|
|
240
|
+
|
|
241
|
+
if not result['is_directory']:
|
|
242
|
+
return result
|
|
243
|
+
|
|
244
|
+
# Check for Python project files
|
|
245
|
+
pyproject_path = os.path.join(dirpath, 'pyproject.toml')
|
|
246
|
+
requirements_path = os.path.join(dirpath, 'requirements.txt')
|
|
247
|
+
|
|
248
|
+
if os.path.exists(pyproject_path):
|
|
249
|
+
result['platform'] = 'python3'
|
|
250
|
+
result['project_type'] = 'pyproject'
|
|
251
|
+
result['project_file'] = pyproject_path
|
|
252
|
+
|
|
253
|
+
# Parse pyproject.toml
|
|
254
|
+
with open(pyproject_path, 'r') as f:
|
|
255
|
+
content = f.read()
|
|
256
|
+
|
|
257
|
+
# Extract project name and version using basic parsing
|
|
258
|
+
# Look for [project] section
|
|
259
|
+
import re
|
|
260
|
+
|
|
261
|
+
# Try to find name in [project] section
|
|
262
|
+
name_match = re.search(r'^\s*name\s*=\s*["\']([^"\']+)["\']', content, re.MULTILINE)
|
|
263
|
+
if name_match:
|
|
264
|
+
result['project_name'] = name_match.group(1)
|
|
265
|
+
|
|
266
|
+
# Try to find version in [project] section
|
|
267
|
+
version_match = re.search(r'^\s*version\s*=\s*["\']([^"\']+)["\']', content, re.MULTILINE)
|
|
268
|
+
if version_match:
|
|
269
|
+
result['project_version'] = version_match.group(1)
|
|
270
|
+
|
|
271
|
+
# Check for fastmcp
|
|
272
|
+
if 'fastmcp' in content:
|
|
273
|
+
result['has_fastmcp'] = True
|
|
274
|
+
version_match = re.search(r'fastmcp[>=<~]*([0-9.]+)', content)
|
|
275
|
+
if version_match:
|
|
276
|
+
result['fastmcp_version'] = version_match.group(1)
|
|
277
|
+
|
|
278
|
+
elif os.path.exists(requirements_path):
|
|
279
|
+
result['platform'] = 'python3'
|
|
280
|
+
result['project_type'] = 'requirements'
|
|
281
|
+
result['project_file'] = requirements_path
|
|
282
|
+
|
|
283
|
+
# Check for fastmcp
|
|
284
|
+
with open(requirements_path, 'r') as f:
|
|
285
|
+
content = f.read()
|
|
286
|
+
if 'fastmcp' in content:
|
|
287
|
+
result['has_fastmcp'] = True
|
|
288
|
+
import re
|
|
289
|
+
version_match = re.search(r'fastmcp[>=<~]*([0-9.]+)', content)
|
|
290
|
+
if version_match:
|
|
291
|
+
result['fastmcp_version'] = version_match.group(1)
|
|
292
|
+
|
|
293
|
+
return result
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def find_project_tools(project_root):
|
|
297
|
+
"""
|
|
298
|
+
Naive recursive search for MCP tools in a Python project.
|
|
299
|
+
Searches all Python files in the project tree, excluding common non-source directories.
|
|
300
|
+
|
|
301
|
+
This is intentionally naive - it doesn't try to be smart about which files to search,
|
|
302
|
+
it just excludes obvious non-source directories and searches everything else.
|
|
303
|
+
"""
|
|
304
|
+
# Common directories to exclude from search
|
|
305
|
+
EXCLUDE_DIRS = {
|
|
306
|
+
'venv', '.venv', 'env', '.env', 'virtualenv',
|
|
307
|
+
'dist', 'build', '__pycache__', '.eggs', 'egg-info',
|
|
308
|
+
'.git', '.pytest_cache', '.mypy_cache', '.tox', 'htmlcov',
|
|
309
|
+
'node_modules', '.coverage', 'site-packages',
|
|
310
|
+
# Also exclude hidden directories
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
results = {
|
|
314
|
+
'project_root': project_root,
|
|
315
|
+
'files_searched': 0,
|
|
316
|
+
'files_with_tools': 0,
|
|
317
|
+
'total_tools': 0,
|
|
318
|
+
'tools': [],
|
|
319
|
+
'errors': []
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
for root, dirs, files in os.walk(project_root):
|
|
323
|
+
# Modify dirs in-place to skip excluded directories
|
|
324
|
+
dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS and not d.startswith('.')]
|
|
325
|
+
|
|
326
|
+
# Process Python files in this directory
|
|
327
|
+
for file in files:
|
|
328
|
+
if file.endswith('.py'):
|
|
329
|
+
filepath = os.path.join(root, file)
|
|
330
|
+
results['files_searched'] += 1
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
file_result = discover_tools_in_file(filepath)
|
|
334
|
+
if file_result.get('success') and file_result.get('tools'):
|
|
335
|
+
results['files_with_tools'] += 1
|
|
336
|
+
for tool in file_result['tools']:
|
|
337
|
+
# Add source file information to each tool
|
|
338
|
+
tool['source_file'] = os.path.relpath(filepath, project_root)
|
|
339
|
+
results['tools'].append(tool)
|
|
340
|
+
results['total_tools'] += 1
|
|
341
|
+
except Exception as e:
|
|
342
|
+
results['errors'].append({
|
|
343
|
+
'file': os.path.relpath(filepath, project_root),
|
|
344
|
+
'error': str(e)
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
return results
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def main():
|
|
351
|
+
if len(sys.argv) < 2:
|
|
352
|
+
print(json.dumps({
|
|
353
|
+
'error': 'Usage: discover_tools.py <command> <path>'
|
|
354
|
+
}))
|
|
355
|
+
sys.exit(1)
|
|
356
|
+
|
|
357
|
+
command = sys.argv[1]
|
|
358
|
+
|
|
359
|
+
if command == 'project':
|
|
360
|
+
if len(sys.argv) < 3:
|
|
361
|
+
print(json.dumps({'error': 'Path required for project discovery'}))
|
|
362
|
+
sys.exit(1)
|
|
363
|
+
path = sys.argv[2]
|
|
364
|
+
result = discover_project(path)
|
|
365
|
+
elif command == 'tools':
|
|
366
|
+
if len(sys.argv) < 3:
|
|
367
|
+
print(json.dumps({'error': 'Path required for tool discovery'}))
|
|
368
|
+
sys.exit(1)
|
|
369
|
+
path = sys.argv[2]
|
|
370
|
+
if os.path.isfile(path):
|
|
371
|
+
result = discover_tools_in_file(path)
|
|
372
|
+
elif os.path.isdir(path):
|
|
373
|
+
result = discover_tools_in_directory(path)
|
|
374
|
+
else:
|
|
375
|
+
result = {'error': f'Path not found: {path}'}
|
|
376
|
+
elif command == 'project-tools':
|
|
377
|
+
if len(sys.argv) < 3:
|
|
378
|
+
print(json.dumps({'error': 'Path required for project tools discovery'}))
|
|
379
|
+
sys.exit(1)
|
|
380
|
+
path = sys.argv[2]
|
|
381
|
+
if os.path.isdir(path):
|
|
382
|
+
result = find_project_tools(path)
|
|
383
|
+
else:
|
|
384
|
+
result = {'error': f'Path is not a directory: {path}'}
|
|
385
|
+
else:
|
|
386
|
+
result = {'error': f'Unknown command: {command}'}
|
|
387
|
+
|
|
388
|
+
print(json.dumps(result, indent=2))
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
if __name__ == '__main__':
|
|
392
|
+
main()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tool type utilities for discovered tools
|
|
3
|
+
* Uses the official Model Context Protocol types from @modelcontextprotocol/sdk
|
|
4
|
+
*/
|
|
5
|
+
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
6
|
+
export type { Tool as MCPTool } from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Convert a discovered Python tool to MCP format
|
|
9
|
+
*/
|
|
10
|
+
export declare function toMCPTool(discoveredTool: any): Tool;
|
|
11
|
+
/**
|
|
12
|
+
* Format tool for OpenAI-compatible function calling
|
|
13
|
+
*/
|
|
14
|
+
export declare function toOpenAIFunction(tool: Tool): {
|
|
15
|
+
type: "function";
|
|
16
|
+
function: {
|
|
17
|
+
name: string;
|
|
18
|
+
description: string | undefined;
|
|
19
|
+
parameters: {
|
|
20
|
+
[x: string]: unknown;
|
|
21
|
+
type: "object";
|
|
22
|
+
properties?: {
|
|
23
|
+
[x: string]: unknown;
|
|
24
|
+
} | undefined;
|
|
25
|
+
required?: string[] | undefined;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tool type utilities for discovered tools
|
|
3
|
+
* Uses the official Model Context Protocol types from @modelcontextprotocol/sdk
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Convert a discovered Python tool to MCP format
|
|
7
|
+
*/
|
|
8
|
+
export function toMCPTool(discoveredTool) {
|
|
9
|
+
// Extract first line of docstring as description
|
|
10
|
+
const description = discoveredTool.docstring
|
|
11
|
+
? discoveredTool.docstring.split('\n')[0].trim()
|
|
12
|
+
: undefined;
|
|
13
|
+
// Build properties from parameters
|
|
14
|
+
const properties = {};
|
|
15
|
+
const required = [];
|
|
16
|
+
if (discoveredTool.parameters) {
|
|
17
|
+
for (const param of discoveredTool.parameters) {
|
|
18
|
+
// Map Python types to JSON Schema types
|
|
19
|
+
let jsonType = 'string';
|
|
20
|
+
if (param.type) {
|
|
21
|
+
const pythonType = param.type.toLowerCase();
|
|
22
|
+
if (pythonType === 'int' || pythonType === 'float') {
|
|
23
|
+
jsonType = 'number';
|
|
24
|
+
}
|
|
25
|
+
else if (pythonType === 'bool') {
|
|
26
|
+
jsonType = 'boolean';
|
|
27
|
+
}
|
|
28
|
+
else if (pythonType.includes('list') ||
|
|
29
|
+
pythonType.includes('array')) {
|
|
30
|
+
jsonType = 'array';
|
|
31
|
+
}
|
|
32
|
+
else if (pythonType.includes('dict') || pythonType === 'object') {
|
|
33
|
+
jsonType = 'object';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
properties[param.name] = {
|
|
37
|
+
type: jsonType,
|
|
38
|
+
description: `Parameter ${param.name}`,
|
|
39
|
+
};
|
|
40
|
+
// For now, assume all parameters are required
|
|
41
|
+
// Could be enhanced to detect optional parameters
|
|
42
|
+
required.push(param.name);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Build the base Tool object
|
|
46
|
+
const tool = {
|
|
47
|
+
name: discoveredTool.name,
|
|
48
|
+
title: toTitleCase(discoveredTool.name),
|
|
49
|
+
description,
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: Object.keys(properties).length > 0 ? properties : undefined,
|
|
53
|
+
required: required.length > 0 ? required : undefined,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
// Add metadata if available
|
|
57
|
+
if (discoveredTool.source_file) {
|
|
58
|
+
tool.source_file = discoveredTool.source_file;
|
|
59
|
+
}
|
|
60
|
+
if (discoveredTool.registered_in) {
|
|
61
|
+
tool.registered_in = discoveredTool.registered_in;
|
|
62
|
+
}
|
|
63
|
+
return tool;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Convert snake_case to Title Case
|
|
67
|
+
*/
|
|
68
|
+
function toTitleCase(snakeCase) {
|
|
69
|
+
return snakeCase
|
|
70
|
+
.split('_')
|
|
71
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
72
|
+
.join(' ');
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Format tool for OpenAI-compatible function calling
|
|
76
|
+
*/
|
|
77
|
+
export function toOpenAIFunction(tool) {
|
|
78
|
+
return {
|
|
79
|
+
type: 'function',
|
|
80
|
+
function: {
|
|
81
|
+
name: tool.name,
|
|
82
|
+
description: tool.description,
|
|
83
|
+
parameters: tool.inputSchema,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|