@agents-at-scale/ark 0.1.34 → 0.1.35-rc.1
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 +50 -0
- package/dist/arkServices.js +153 -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 +2 -0
- package/dist/commands/agents/index.js +56 -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 +2 -0
- package/dist/commands/chat/index.js +45 -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/index.js +2 -4
- package/dist/commands/completion/index.d.ts +2 -0
- package/dist/commands/completion/index.js +268 -0
- package/dist/commands/completion.js +159 -2
- package/dist/commands/config/index.d.ts +2 -0
- package/dist/commands/config/index.js +42 -0
- package/dist/commands/config.d.ts +0 -3
- package/dist/commands/config.js +38 -321
- package/dist/commands/dashboard/index.d.ts +3 -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 +2 -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 +256 -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.d.ts +2 -0
- package/dist/commands/dev/tool.js +559 -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/install/index.d.ts +6 -0
- package/dist/commands/install/index.js +165 -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/index.d.ts +2 -0
- package/dist/commands/models/index.js +65 -0
- package/dist/commands/models/selector.d.ts +8 -0
- package/dist/commands/models/selector.js +53 -0
- package/dist/commands/routes/index.d.ts +2 -0
- package/dist/commands/routes/index.js +101 -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 +33 -0
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.js +33 -0
- package/dist/commands/targets/index.d.ts +2 -0
- package/dist/commands/targets/index.js +65 -0
- package/dist/commands/targets.d.ts +2 -0
- package/dist/commands/targets.js +65 -0
- package/dist/commands/teams/index.d.ts +2 -0
- package/dist/commands/teams/index.js +54 -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 +2 -0
- package/dist/commands/tools/index.js +54 -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 +2 -0
- package/dist/commands/uninstall/index.js +84 -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 +10 -13
- package/dist/components/statusChecker.js +128 -65
- package/dist/config.js +3 -10
- package/dist/index.d.ts +1 -1
- package/dist/index.js +31 -36
- 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 +93 -0
- package/dist/lib/arkStatus.d.ts +5 -0
- package/dist/lib/arkStatus.js +20 -0
- package/dist/lib/chatClient.d.ts +33 -0
- package/dist/lib/chatClient.js +101 -0
- package/dist/lib/cluster.d.ts +2 -1
- package/dist/lib/cluster.js +27 -3
- 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/config.d.ts +24 -80
- package/dist/lib/config.js +68 -205
- package/dist/lib/config.test.d.ts +1 -0
- package/dist/lib/config.test.js +93 -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/output.d.ts +36 -0
- package/dist/lib/output.js +89 -0
- package/dist/lib/types.d.ts +8 -3
- package/dist/types/types.d.ts +40 -0
- package/dist/types/types.js +1 -0
- package/dist/ui/MainMenu.js +158 -90
- package/dist/ui/statusFormatter.d.ts +4 -1
- package/dist/ui/statusFormatter.js +91 -19
- package/package.json +16 -4
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface ToolParameter {
|
|
2
|
+
name: string;
|
|
3
|
+
type?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface DiscoveredTool {
|
|
6
|
+
name: string;
|
|
7
|
+
parameters: ToolParameter[];
|
|
8
|
+
return_type?: string;
|
|
9
|
+
docstring?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface FileDiscoveryResult {
|
|
12
|
+
success: boolean;
|
|
13
|
+
file: string;
|
|
14
|
+
tools: DiscoveredTool[];
|
|
15
|
+
uses_fastmcp: boolean;
|
|
16
|
+
mcp_instance?: string;
|
|
17
|
+
server_name?: string;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface DirectoryDiscoveryResult {
|
|
21
|
+
directory: string;
|
|
22
|
+
files: FileDiscoveryResult[];
|
|
23
|
+
total_tools: number;
|
|
24
|
+
uses_fastmcp: boolean;
|
|
25
|
+
}
|
|
26
|
+
export type DiscoveryResult = FileDiscoveryResult | DirectoryDiscoveryResult;
|
|
27
|
+
export interface ProjectDiscoveryResult {
|
|
28
|
+
path: string;
|
|
29
|
+
exists: boolean;
|
|
30
|
+
is_directory: boolean;
|
|
31
|
+
platform: 'python3' | null;
|
|
32
|
+
project_type: 'pyproject' | 'requirements' | null;
|
|
33
|
+
project_file: string | null;
|
|
34
|
+
project_name: string | null;
|
|
35
|
+
project_version: string | null;
|
|
36
|
+
has_fastmcp: boolean;
|
|
37
|
+
fastmcp_version: string | null;
|
|
38
|
+
}
|
|
39
|
+
export interface ProjectInfo {
|
|
40
|
+
path: string;
|
|
41
|
+
platform: 'python3';
|
|
42
|
+
projectType: 'pyproject' | 'requirements' | 'unknown';
|
|
43
|
+
hasVenv: boolean;
|
|
44
|
+
fastMCP: boolean;
|
|
45
|
+
fastMCPVersion?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface ArkDevToolStatus extends ProjectInfo {
|
|
48
|
+
discovery?: DiscoveryResult;
|
|
49
|
+
tools: DiscoveredTool[];
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type StatusType = 'success' | 'warning' | 'info' | 'error';
|
|
2
|
+
declare const output: {
|
|
3
|
+
/**
|
|
4
|
+
* Display a status message with flexible formatting
|
|
5
|
+
*/
|
|
6
|
+
statusMessage(type: StatusType, title: string, message?: string, ...args: unknown[]): void;
|
|
7
|
+
/**
|
|
8
|
+
* Display an error message with consistent formatting
|
|
9
|
+
*/
|
|
10
|
+
error(message: string, ...args: unknown[]): void;
|
|
11
|
+
/**
|
|
12
|
+
* Display a success message with consistent formatting
|
|
13
|
+
*/
|
|
14
|
+
success(message: string, ...args: unknown[]): void;
|
|
15
|
+
/**
|
|
16
|
+
* Display an info message (indented gray text)
|
|
17
|
+
*/
|
|
18
|
+
info(message: string, ...args: unknown[]): void;
|
|
19
|
+
/**
|
|
20
|
+
* Display a warning message with consistent formatting
|
|
21
|
+
*/
|
|
22
|
+
warning(message: string, ...args: unknown[]): void;
|
|
23
|
+
/**
|
|
24
|
+
* Display a status check item (like ark status format)
|
|
25
|
+
* @param status - 'found', 'missing', 'warning', 'error'
|
|
26
|
+
* @param label - The label to show (e.g., 'platform')
|
|
27
|
+
* @param value - The value in bright white (e.g., 'python3')
|
|
28
|
+
* @param details - Optional grey details
|
|
29
|
+
*/
|
|
30
|
+
statusCheck(status: "found" | "missing" | "warning" | "error", label: string, value?: string, details?: string): void;
|
|
31
|
+
/**
|
|
32
|
+
* Display a section header (like 'ark services:')
|
|
33
|
+
*/
|
|
34
|
+
section(title: string): void;
|
|
35
|
+
};
|
|
36
|
+
export default output;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
const output = {
|
|
3
|
+
/**
|
|
4
|
+
* Display a status message with flexible formatting
|
|
5
|
+
*/
|
|
6
|
+
statusMessage(type, title, message, ...args) {
|
|
7
|
+
const icons = {
|
|
8
|
+
success: chalk.green('✓'),
|
|
9
|
+
warning: chalk.yellow.bold('!'),
|
|
10
|
+
info: chalk.blue('ℹ'),
|
|
11
|
+
error: chalk.red('✗'),
|
|
12
|
+
};
|
|
13
|
+
const colors = {
|
|
14
|
+
success: chalk.green,
|
|
15
|
+
warning: chalk.yellow,
|
|
16
|
+
info: chalk.blue,
|
|
17
|
+
error: chalk.red,
|
|
18
|
+
};
|
|
19
|
+
const icon = icons[type];
|
|
20
|
+
const color = colors[type];
|
|
21
|
+
const logFn = type === 'error' ? console.error : console.log;
|
|
22
|
+
if (message) {
|
|
23
|
+
logFn(icon, color(`${title}:`), message, ...args);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
logFn(icon, title, ...args);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
/**
|
|
30
|
+
* Display an error message with consistent formatting
|
|
31
|
+
*/
|
|
32
|
+
error(message, ...args) {
|
|
33
|
+
this.statusMessage('error', 'error', message, ...args);
|
|
34
|
+
},
|
|
35
|
+
/**
|
|
36
|
+
* Display a success message with consistent formatting
|
|
37
|
+
*/
|
|
38
|
+
success(message, ...args) {
|
|
39
|
+
this.statusMessage('success', message, undefined, ...args);
|
|
40
|
+
},
|
|
41
|
+
/**
|
|
42
|
+
* Display an info message (indented gray text)
|
|
43
|
+
*/
|
|
44
|
+
info(message, ...args) {
|
|
45
|
+
console.log(chalk.gray(message), ...args);
|
|
46
|
+
},
|
|
47
|
+
/**
|
|
48
|
+
* Display a warning message with consistent formatting
|
|
49
|
+
*/
|
|
50
|
+
warning(message, ...args) {
|
|
51
|
+
this.statusMessage('warning', 'warning', message, ...args);
|
|
52
|
+
},
|
|
53
|
+
/**
|
|
54
|
+
* Display a status check item (like ark status format)
|
|
55
|
+
* @param status - 'found', 'missing', 'warning', 'error'
|
|
56
|
+
* @param label - The label to show (e.g., 'platform')
|
|
57
|
+
* @param value - The value in bright white (e.g., 'python3')
|
|
58
|
+
* @param details - Optional grey details
|
|
59
|
+
*/
|
|
60
|
+
statusCheck(status, label, value, details) {
|
|
61
|
+
const icons = {
|
|
62
|
+
found: chalk.green('✓'),
|
|
63
|
+
missing: chalk.yellow('?'),
|
|
64
|
+
warning: chalk.yellow('!'),
|
|
65
|
+
error: chalk.red('✗'),
|
|
66
|
+
};
|
|
67
|
+
const statusText = {
|
|
68
|
+
found: chalk.green(label),
|
|
69
|
+
missing: chalk.yellow(label),
|
|
70
|
+
warning: chalk.yellow(label),
|
|
71
|
+
error: chalk.red(label),
|
|
72
|
+
};
|
|
73
|
+
let output = ` ${icons[status]} ${statusText[status]}`;
|
|
74
|
+
if (value) {
|
|
75
|
+
output += ` ${chalk.bold.white(value)}`;
|
|
76
|
+
}
|
|
77
|
+
if (details) {
|
|
78
|
+
output += chalk.gray(` ${details}`);
|
|
79
|
+
}
|
|
80
|
+
console.log(output);
|
|
81
|
+
},
|
|
82
|
+
/**
|
|
83
|
+
* Display a section header (like 'ark services:')
|
|
84
|
+
*/
|
|
85
|
+
section(title) {
|
|
86
|
+
console.log(chalk.cyan.bold(`${title}:`));
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
export default output;
|