first commit
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .DS_Store +0 -0
- Hypoalphalipoproteinemia_variant.vcf +2 -0
- __pycache__/agent_v3.cpython-311.pyc +0 -0
- a.ipynb +0 -71
- agent_v3.py +0 -2
- app.py +32 -11
- core/__pycache__/__init__.cpython-311.pyc +0 -0
- core/__pycache__/{__init__.cpython-310.pyc → __init__.cpython-312.pyc} +0 -0
- core/__pycache__/constants.cpython-311.pyc +0 -0
- core/__pycache__/{constants.cpython-310.pyc → constants.cpython-312.pyc} +0 -0
- core/__pycache__/types.cpython-310.pyc +0 -0
- core/__pycache__/types.cpython-311.pyc +0 -0
- core/__pycache__/types.cpython-312.pyc +0 -0
- images.png +0 -0
- managers/.DS_Store +0 -0
- managers/__pycache__/__init__.cpython-311.pyc +0 -0
- managers/execution/__pycache__/__init__.cpython-310.pyc +0 -0
- managers/execution/__pycache__/__init__.cpython-311.pyc +0 -0
- managers/execution/__pycache__/monitoring.cpython-310.pyc +0 -0
- managers/execution/__pycache__/monitoring.cpython-311.pyc +0 -0
- managers/execution/__pycache__/python_executor.cpython-310.pyc +0 -0
- managers/execution/__pycache__/python_executor.cpython-311.pyc +0 -0
- managers/support/__pycache__/__init__.cpython-310.pyc +0 -0
- managers/support/__pycache__/__init__.cpython-311.pyc +0 -0
- managers/support/__pycache__/console_display.cpython-310.pyc +0 -0
- managers/support/__pycache__/console_display.cpython-311.pyc +0 -0
- managers/support/__pycache__/package_manager.cpython-310.pyc +0 -0
- managers/support/__pycache__/package_manager.cpython-311.pyc +0 -0
- managers/tools/__pycache__/__init__.cpython-311.pyc +0 -0
- managers/tools/__pycache__/builtin_tools.cpython-311.pyc +0 -0
- managers/tools/__pycache__/mcp_manager.cpython-311.pyc +0 -0
- managers/tools/__pycache__/tool_manager.cpython-311.pyc +0 -0
- managers/tools/__pycache__/tool_registry.cpython-311.pyc +0 -0
- managers/tools/__pycache__/tool_selector.cpython-311.pyc +0 -0
- managers/tools/mcp_manager.py +300 -592
- managers/workflow/__pycache__/__init__.cpython-310.pyc +0 -0
- managers/workflow/__pycache__/__init__.cpython-311.pyc +0 -0
- managers/workflow/__pycache__/plan_manager.cpython-310.pyc +0 -0
- managers/workflow/__pycache__/plan_manager.cpython-311.pyc +0 -0
- managers/workflow/__pycache__/state_manager.cpython-310.pyc +0 -0
- managers/workflow/__pycache__/state_manager.cpython-311.pyc +0 -0
- managers/workflow/__pycache__/workflow_engine.cpython-310.pyc +0 -0
- managers/workflow/__pycache__/workflow_engine.cpython-311.pyc +0 -0
- tools/__pycache__/batch_variant_scoring.cpython-311.pyc +0 -0
- tools/__pycache__/batch_variant_scoring.cpython-312.pyc +0 -0
- tools/__pycache__/essential_commands.cpython-311.pyc +0 -0
- traces/agent_summary_20251006_150052.json +0 -83
- traces/agent_summary_20251006_154956.json +0 -47
- traces/agent_summary_20251006_155159.json +0 -83
- traces/agent_summary_20251007_154340.json +71 -0
.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
Hypoalphalipoproteinemia_variant.vcf
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
variant_id CHROM POS REF ALT
|
| 2 |
+
Hypoalphalipoproteinemia_variant chr11 116837649 T G
|
__pycache__/agent_v3.cpython-311.pyc
ADDED
|
Binary file (28.5 kB). View file
|
|
|
a.ipynb
DELETED
|
@@ -1,71 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"cells": [
|
| 3 |
-
{
|
| 4 |
-
"cell_type": "code",
|
| 5 |
-
"execution_count": null,
|
| 6 |
-
"id": "2677cc9c",
|
| 7 |
-
"metadata": {},
|
| 8 |
-
"outputs": [
|
| 9 |
-
{
|
| 10 |
-
"ename": "HTTPError",
|
| 11 |
-
"evalue": "405 Client Error: Method Not Allowed for url: https://alphagenome.fastmcp.app/mcp/tools/list",
|
| 12 |
-
"output_type": "error",
|
| 13 |
-
"traceback": [
|
| 14 |
-
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
|
| 15 |
-
"\u001b[0;31mHTTPError\u001b[0m Traceback (most recent call last)",
|
| 16 |
-
"Cell \u001b[0;32mIn[1], line 6\u001b[0m\n\u001b[1;32m 4\u001b[0m mcp_url \u001b[38;5;241m=\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mhttps://AlphaGenome.fastmcp.app/mcp/tools/list\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 5\u001b[0m resp \u001b[38;5;241m=\u001b[39m requests\u001b[38;5;241m.\u001b[39mget(mcp_url)\n\u001b[0;32m----> 6\u001b[0m resp\u001b[38;5;241m.\u001b[39mraise_for_status()\n\u001b[1;32m 7\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;124m🧰 MCP /tools/list response:\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 8\u001b[0m \u001b[38;5;28mprint\u001b[39m(resp\u001b[38;5;241m.\u001b[39mjson())\n",
|
| 17 |
-
"File \u001b[0;32m/opt/anaconda3/lib/python3.12/site-packages/requests/models.py:1024\u001b[0m, in \u001b[0;36mResponse.raise_for_status\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 1019\u001b[0m http_error_msg \u001b[38;5;241m=\u001b[39m (\n\u001b[1;32m 1020\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mstatus_code\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m Server Error: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mreason\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m for url: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39murl\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 1021\u001b[0m )\n\u001b[1;32m 1023\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m http_error_msg:\n\u001b[0;32m-> 1024\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m HTTPError(http_error_msg, response\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mself\u001b[39m)\n",
|
| 18 |
-
"\u001b[0;31mHTTPError\u001b[0m: 405 Client Error: Method Not Allowed for url: https://alphagenome.fastmcp.app/mcp/tools/list"
|
| 19 |
-
]
|
| 20 |
-
}
|
| 21 |
-
],
|
| 22 |
-
"source": [
|
| 23 |
-
"import requests\n",
|
| 24 |
-
"\n",
|
| 25 |
-
"def list_mcp_tools(url):\n",
|
| 26 |
-
" payload = {\n",
|
| 27 |
-
" \"jsonrpc\": \"2.0\",\n",
|
| 28 |
-
" \"id\": 1,\n",
|
| 29 |
-
" \"method\": \"tools/list\",\n",
|
| 30 |
-
" \"params\": {}\n",
|
| 31 |
-
" }\n",
|
| 32 |
-
"\n",
|
| 33 |
-
" response = requests.post(url, json=payload)\n",
|
| 34 |
-
" return response.json()\n",
|
| 35 |
-
"\n",
|
| 36 |
-
"# Usage\n",
|
| 37 |
-
"tools = list_mcp_tools(\"https://alphagenome.fastmcp.app/mcp\")\n",
|
| 38 |
-
"print(tools)"
|
| 39 |
-
]
|
| 40 |
-
},
|
| 41 |
-
{
|
| 42 |
-
"cell_type": "code",
|
| 43 |
-
"execution_count": null,
|
| 44 |
-
"id": "b3f82208",
|
| 45 |
-
"metadata": {},
|
| 46 |
-
"outputs": [],
|
| 47 |
-
"source": []
|
| 48 |
-
}
|
| 49 |
-
],
|
| 50 |
-
"metadata": {
|
| 51 |
-
"kernelspec": {
|
| 52 |
-
"display_name": "base",
|
| 53 |
-
"language": "python",
|
| 54 |
-
"name": "python3"
|
| 55 |
-
},
|
| 56 |
-
"language_info": {
|
| 57 |
-
"codemirror_mode": {
|
| 58 |
-
"name": "ipython",
|
| 59 |
-
"version": 3
|
| 60 |
-
},
|
| 61 |
-
"file_extension": ".py",
|
| 62 |
-
"mimetype": "text/x-python",
|
| 63 |
-
"name": "python",
|
| 64 |
-
"nbconvert_exporter": "python",
|
| 65 |
-
"pygments_lexer": "ipython3",
|
| 66 |
-
"version": "3.12.2"
|
| 67 |
-
}
|
| 68 |
-
},
|
| 69 |
-
"nbformat": 4,
|
| 70 |
-
"nbformat_minor": 5
|
| 71 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
agent_v3.py
CHANGED
|
@@ -556,10 +556,8 @@ if __name__ == "__main__":
|
|
| 556 |
print(f"⚠️ Tool validation issues: {issues}")
|
| 557 |
else:
|
| 558 |
print("✅ All tools validated successfully!")
|
| 559 |
-
|
| 560 |
# List available packages
|
| 561 |
print(f"\n📦 Available packages: {list(agent.get_all_packages().keys())}")
|
| 562 |
-
|
| 563 |
# Run the agent with a query and save trace/summary
|
| 564 |
print("\n🚀 Running agent with trace and summary saving...")
|
| 565 |
result = agent.run(
|
|
|
|
| 556 |
print(f"⚠️ Tool validation issues: {issues}")
|
| 557 |
else:
|
| 558 |
print("✅ All tools validated successfully!")
|
|
|
|
| 559 |
# List available packages
|
| 560 |
print(f"\n📦 Available packages: {list(agent.get_all_packages().keys())}")
|
|
|
|
| 561 |
# Run the agent with a query and save trace/summary
|
| 562 |
print("\n🚀 Running agent with trace and summary saving...")
|
| 563 |
result = agent.run(
|
app.py
CHANGED
|
@@ -65,9 +65,9 @@ class GradioAgentUI:
|
|
| 65 |
|
| 66 |
self.model = model
|
| 67 |
self.config = config
|
| 68 |
-
self.name = "
|
| 69 |
-
self.description = "A Code Agent that can execute Python code and use tools to
|
| 70 |
-
|
| 71 |
def get_step_footnote(self, step_num: int, duration: float) -> str:
|
| 72 |
"""Create a footnote for a step with timing information."""
|
| 73 |
return f'<span style="color: #888; font-size: 0.9em;">Step {step_num} | Duration: {duration:.2f}s</span>'
|
|
@@ -413,8 +413,19 @@ class GradioAgentUI:
|
|
| 413 |
font_mono=fonts.GoogleFont("JetBrains Mono")
|
| 414 |
)
|
| 415 |
|
| 416 |
-
with gr.Blocks(theme=modern_theme, fill_height=True, title=self.name
|
| 417 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 418 |
demo.load(js="""
|
| 419 |
() => {
|
| 420 |
document.body.classList.remove('dark');
|
|
@@ -422,6 +433,16 @@ class GradioAgentUI:
|
|
| 422 |
const url = new URL(window.location);
|
| 423 |
url.searchParams.set('__theme', 'light');
|
| 424 |
window.history.replaceState({}, '', url);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 425 |
}
|
| 426 |
""")
|
| 427 |
# Session state
|
|
@@ -476,16 +497,14 @@ for the variant chr11:116837649:T>G, associated with Hypoalphalipoproteinemia. M
|
|
| 476 |
["Analyze the genetic variants associated with Type 2 Diabetes."],
|
| 477 |
["Find the top drug targets for inflammatory bowel disease."],
|
| 478 |
["What are the functional consequences of mutations in the BRCA1 gene?"],
|
| 479 |
-
["Calculate the factorial of 10"],
|
| 480 |
-
["Create a simple plot of y = x^2 from -10 to 10"],
|
| 481 |
],
|
| 482 |
inputs=text_input,
|
| 483 |
)
|
| 484 |
|
| 485 |
gr.Markdown("---")
|
| 486 |
-
gr.HTML(
|
| 487 |
-
|
| 488 |
-
)
|
| 489 |
|
| 490 |
# Main chat area
|
| 491 |
with gr.Column(scale=3):
|
|
@@ -496,7 +515,7 @@ for the variant chr11:116837649:T>G, associated with Hypoalphalipoproteinemia. M
|
|
| 496 |
show_copy_button=True,
|
| 497 |
avatar_images=(
|
| 498 |
None, # Default user avatar
|
| 499 |
-
"
|
| 500 |
),
|
| 501 |
latex_delimiters=[
|
| 502 |
{"left": r"$$", "right": r"$$", "display": True},
|
|
@@ -579,6 +598,8 @@ for the variant chr11:116837649:T>G, associated with Hypoalphalipoproteinemia. M
|
|
| 579 |
|
| 580 |
app.queue(max_size=10).launch(
|
| 581 |
share=share,
|
|
|
|
|
|
|
| 582 |
**kwargs
|
| 583 |
)
|
| 584 |
|
|
|
|
| 65 |
|
| 66 |
self.model = model
|
| 67 |
self.config = config
|
| 68 |
+
self.name = "AlphaGenome Agent Interface"
|
| 69 |
+
self.description = "A Code Agent that can execute Python code and use tools to decipher the regulatory code within DNA sequences."
|
| 70 |
+
|
| 71 |
def get_step_footnote(self, step_num: int, duration: float) -> str:
|
| 72 |
"""Create a footnote for a step with timing information."""
|
| 73 |
return f'<span style="color: #888; font-size: 0.9em;">Step {step_num} | Duration: {duration:.2f}s</span>'
|
|
|
|
| 413 |
font_mono=fonts.GoogleFont("JetBrains Mono")
|
| 414 |
)
|
| 415 |
|
| 416 |
+
with gr.Blocks(theme=modern_theme, fill_height=True, title=self.name, css="""
|
| 417 |
+
/* Hide Gradio footer */
|
| 418 |
+
.footer {display: none !important;}
|
| 419 |
+
footer {display: none !important;}
|
| 420 |
+
.gradio-footer {display: none !important;}
|
| 421 |
+
#footer {display: none !important;}
|
| 422 |
+
[class*="footer"] {display: none !important;}
|
| 423 |
+
[id*="footer"] {display: none !important;}
|
| 424 |
+
.block.svelte-1scc9gv {display: none !important;}
|
| 425 |
+
.built-with-gradio {display: none !important;}
|
| 426 |
+
.gradio-container footer {display: none !important;}
|
| 427 |
+
""") as demo:
|
| 428 |
+
# Force light theme and hide footer
|
| 429 |
demo.load(js="""
|
| 430 |
() => {
|
| 431 |
document.body.classList.remove('dark');
|
|
|
|
| 433 |
const url = new URL(window.location);
|
| 434 |
url.searchParams.set('__theme', 'light');
|
| 435 |
window.history.replaceState({}, '', url);
|
| 436 |
+
|
| 437 |
+
// Hide footer elements
|
| 438 |
+
setTimeout(() => {
|
| 439 |
+
const footers = document.querySelectorAll('footer, .footer, .gradio-footer, #footer, [class*="footer"], [id*="footer"], .built-with-gradio');
|
| 440 |
+
footers.forEach(footer => footer.style.display = 'none');
|
| 441 |
+
|
| 442 |
+
// Also hide any Gradio logo/branding
|
| 443 |
+
const brandingElements = document.querySelectorAll('a[href*="gradio"], .gradio-logo, [alt*="gradio"]');
|
| 444 |
+
brandingElements.forEach(el => el.style.display = 'none');
|
| 445 |
+
}, 100);
|
| 446 |
}
|
| 447 |
""")
|
| 448 |
# Session state
|
|
|
|
| 497 |
["Analyze the genetic variants associated with Type 2 Diabetes."],
|
| 498 |
["Find the top drug targets for inflammatory bowel disease."],
|
| 499 |
["What are the functional consequences of mutations in the BRCA1 gene?"],
|
|
|
|
|
|
|
| 500 |
],
|
| 501 |
inputs=text_input,
|
| 502 |
)
|
| 503 |
|
| 504 |
gr.Markdown("---")
|
| 505 |
+
# gr.HTML(
|
| 506 |
+
# "<center><small>Powered by LangGraph & OpenRouter</small></center>"
|
| 507 |
+
# )
|
| 508 |
|
| 509 |
# Main chat area
|
| 510 |
with gr.Column(scale=3):
|
|
|
|
| 515 |
show_copy_button=True,
|
| 516 |
avatar_images=(
|
| 517 |
None, # Default user avatar
|
| 518 |
+
"images.png" # Assistant avatar
|
| 519 |
),
|
| 520 |
latex_delimiters=[
|
| 521 |
{"left": r"$$", "right": r"$$", "display": True},
|
|
|
|
| 598 |
|
| 599 |
app.queue(max_size=10).launch(
|
| 600 |
share=share,
|
| 601 |
+
show_api=False, # 隐藏 "Use via API" 链接
|
| 602 |
+
favicon_path=None, # 移除默认favicon
|
| 603 |
**kwargs
|
| 604 |
)
|
| 605 |
|
core/__pycache__/__init__.cpython-311.pyc
CHANGED
|
Binary files a/core/__pycache__/__init__.cpython-311.pyc and b/core/__pycache__/__init__.cpython-311.pyc differ
|
|
|
core/__pycache__/{__init__.cpython-310.pyc → __init__.cpython-312.pyc}
RENAMED
|
Binary files a/core/__pycache__/__init__.cpython-310.pyc and b/core/__pycache__/__init__.cpython-312.pyc differ
|
|
|
core/__pycache__/constants.cpython-311.pyc
CHANGED
|
Binary files a/core/__pycache__/constants.cpython-311.pyc and b/core/__pycache__/constants.cpython-311.pyc differ
|
|
|
core/__pycache__/{constants.cpython-310.pyc → constants.cpython-312.pyc}
RENAMED
|
Binary files a/core/__pycache__/constants.cpython-310.pyc and b/core/__pycache__/constants.cpython-312.pyc differ
|
|
|
core/__pycache__/types.cpython-310.pyc
DELETED
|
Binary file (1.3 kB)
|
|
|
core/__pycache__/types.cpython-311.pyc
CHANGED
|
Binary files a/core/__pycache__/types.cpython-311.pyc and b/core/__pycache__/types.cpython-311.pyc differ
|
|
|
core/__pycache__/types.cpython-312.pyc
ADDED
|
Binary file (1.49 kB). View file
|
|
|
images.png
ADDED
|
managers/.DS_Store
CHANGED
|
Binary files a/managers/.DS_Store and b/managers/.DS_Store differ
|
|
|
managers/__pycache__/__init__.cpython-311.pyc
CHANGED
|
Binary files a/managers/__pycache__/__init__.cpython-311.pyc and b/managers/__pycache__/__init__.cpython-311.pyc differ
|
|
|
managers/execution/__pycache__/__init__.cpython-310.pyc
DELETED
|
Binary file (389 Bytes)
|
|
|
managers/execution/__pycache__/__init__.cpython-311.pyc
CHANGED
|
Binary files a/managers/execution/__pycache__/__init__.cpython-311.pyc and b/managers/execution/__pycache__/__init__.cpython-311.pyc differ
|
|
|
managers/execution/__pycache__/monitoring.cpython-310.pyc
DELETED
|
Binary file (1.77 kB)
|
|
|
managers/execution/__pycache__/monitoring.cpython-311.pyc
CHANGED
|
Binary files a/managers/execution/__pycache__/monitoring.cpython-311.pyc and b/managers/execution/__pycache__/monitoring.cpython-311.pyc differ
|
|
|
managers/execution/__pycache__/python_executor.cpython-310.pyc
DELETED
|
Binary file (4.88 kB)
|
|
|
managers/execution/__pycache__/python_executor.cpython-311.pyc
CHANGED
|
Binary files a/managers/execution/__pycache__/python_executor.cpython-311.pyc and b/managers/execution/__pycache__/python_executor.cpython-311.pyc differ
|
|
|
managers/support/__pycache__/__init__.cpython-310.pyc
DELETED
|
Binary file (377 Bytes)
|
|
|
managers/support/__pycache__/__init__.cpython-311.pyc
CHANGED
|
Binary files a/managers/support/__pycache__/__init__.cpython-311.pyc and b/managers/support/__pycache__/__init__.cpython-311.pyc differ
|
|
|
managers/support/__pycache__/console_display.cpython-310.pyc
DELETED
|
Binary file (5.54 kB)
|
|
|
managers/support/__pycache__/console_display.cpython-311.pyc
CHANGED
|
Binary files a/managers/support/__pycache__/console_display.cpython-311.pyc and b/managers/support/__pycache__/console_display.cpython-311.pyc differ
|
|
|
managers/support/__pycache__/package_manager.cpython-310.pyc
DELETED
|
Binary file (2.33 kB)
|
|
|
managers/support/__pycache__/package_manager.cpython-311.pyc
CHANGED
|
Binary files a/managers/support/__pycache__/package_manager.cpython-311.pyc and b/managers/support/__pycache__/package_manager.cpython-311.pyc differ
|
|
|
managers/tools/__pycache__/__init__.cpython-311.pyc
CHANGED
|
Binary files a/managers/tools/__pycache__/__init__.cpython-311.pyc and b/managers/tools/__pycache__/__init__.cpython-311.pyc differ
|
|
|
managers/tools/__pycache__/builtin_tools.cpython-311.pyc
CHANGED
|
Binary files a/managers/tools/__pycache__/builtin_tools.cpython-311.pyc and b/managers/tools/__pycache__/builtin_tools.cpython-311.pyc differ
|
|
|
managers/tools/__pycache__/mcp_manager.cpython-311.pyc
CHANGED
|
Binary files a/managers/tools/__pycache__/mcp_manager.cpython-311.pyc and b/managers/tools/__pycache__/mcp_manager.cpython-311.pyc differ
|
|
|
managers/tools/__pycache__/tool_manager.cpython-311.pyc
CHANGED
|
Binary files a/managers/tools/__pycache__/tool_manager.cpython-311.pyc and b/managers/tools/__pycache__/tool_manager.cpython-311.pyc differ
|
|
|
managers/tools/__pycache__/tool_registry.cpython-311.pyc
CHANGED
|
Binary files a/managers/tools/__pycache__/tool_registry.cpython-311.pyc and b/managers/tools/__pycache__/tool_registry.cpython-311.pyc differ
|
|
|
managers/tools/__pycache__/tool_selector.cpython-311.pyc
CHANGED
|
Binary files a/managers/tools/__pycache__/tool_selector.cpython-311.pyc and b/managers/tools/__pycache__/tool_selector.cpython-311.pyc differ
|
|
|
managers/tools/mcp_manager.py
CHANGED
|
@@ -1,19 +1,14 @@
|
|
| 1 |
"""
|
| 2 |
MCP Manager for CodeAct Agent.
|
| 3 |
Manages MCP (Model Context Protocol) tools and servers.
|
| 4 |
-
Supports both local (stdio) and remote (HTTP/WebSocket) MCP servers.
|
| 5 |
"""
|
| 6 |
|
| 7 |
import os
|
| 8 |
import sys
|
| 9 |
-
import ssl
|
| 10 |
import types
|
| 11 |
-
import asyncio
|
| 12 |
-
import logging
|
| 13 |
from pathlib import Path
|
| 14 |
-
from typing import Dict, List, Optional, Any
|
| 15 |
from rich.console import Console
|
| 16 |
-
from urllib.parse import urlparse
|
| 17 |
|
| 18 |
|
| 19 |
class MCPManager:
|
|
@@ -22,13 +17,6 @@ class MCPManager:
|
|
| 22 |
def __init__(self, console_display=None):
|
| 23 |
self.mcp_functions = {}
|
| 24 |
self.console = console_display.console if console_display else Console()
|
| 25 |
-
self.remote_connections = {} # Track active remote connections
|
| 26 |
-
self.logger = logging.getLogger(__name__)
|
| 27 |
-
|
| 28 |
-
# Connection defaults
|
| 29 |
-
self.default_timeout = 30
|
| 30 |
-
self.default_retries = 3
|
| 31 |
-
self.retry_delay = 1.0
|
| 32 |
|
| 33 |
def has_mcp_functions(self) -> bool:
|
| 34 |
"""Check if MCP functions are available."""
|
|
@@ -48,504 +36,78 @@ class MCPManager:
|
|
| 48 |
return servers
|
| 49 |
|
| 50 |
def add_mcp(self, config_path: str = "./mcp_config.yaml", tool_registry=None) -> None:
|
| 51 |
-
"""Add MCP tools from configuration file
|
| 52 |
try:
|
|
|
|
| 53 |
import yaml
|
| 54 |
except ImportError as e:
|
| 55 |
raise ImportError(f"Required packages not available: {e}. Install with: pip install pyyaml") from e
|
| 56 |
|
| 57 |
try:
|
| 58 |
import nest_asyncio
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
nest_asyncio.apply()
|
| 60 |
except ImportError as e:
|
| 61 |
-
raise ImportError(f"MCP packages not available: {e}. Install with: pip install mcp
|
| 62 |
-
|
| 63 |
-
# Load and validate configuration
|
| 64 |
-
try:
|
| 65 |
-
config_content = Path(config_path).read_text(encoding="utf-8")
|
| 66 |
-
cfg = yaml.safe_load(config_content) or {}
|
| 67 |
-
except FileNotFoundError:
|
| 68 |
-
raise FileNotFoundError(f"MCP config file not found: {config_path}") from None
|
| 69 |
-
except yaml.YAMLError as e:
|
| 70 |
-
raise yaml.YAMLError(f"Invalid YAML in MCP config: {e}") from e
|
| 71 |
-
|
| 72 |
-
mcp_servers = cfg.get("mcp_servers", {})
|
| 73 |
-
if not mcp_servers:
|
| 74 |
-
print("Warning: No MCP servers found in configuration")
|
| 75 |
-
return
|
| 76 |
-
|
| 77 |
-
# Process each MCP server configuration
|
| 78 |
-
for server_name, server_meta in mcp_servers.items():
|
| 79 |
-
if not server_meta.get("enabled", True):
|
| 80 |
-
continue
|
| 81 |
-
|
| 82 |
-
# Check if it's a remote server (has URL) or local server (has command)
|
| 83 |
-
server_url = server_meta.get("url")
|
| 84 |
-
cmd_list = server_meta.get("command", [])
|
| 85 |
-
|
| 86 |
-
if server_url:
|
| 87 |
-
# Remote server
|
| 88 |
-
self._add_remote_mcp_server(server_name, server_meta, tool_registry)
|
| 89 |
-
elif cmd_list and isinstance(cmd_list, list):
|
| 90 |
-
# Local server
|
| 91 |
-
self._add_local_mcp_server(server_name, server_meta, tool_registry)
|
| 92 |
-
else:
|
| 93 |
-
print(f"Warning: Server '{server_name}' has neither URL nor valid command configuration")
|
| 94 |
-
continue
|
| 95 |
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
async def _create_session_with_auth(self, server_meta: dict) -> 'aiohttp.ClientSession':
|
| 99 |
-
"""Create an authenticated HTTP session for remote MCP server."""
|
| 100 |
-
import aiohttp
|
| 101 |
-
|
| 102 |
-
# Set up authentication headers
|
| 103 |
-
headers = {}
|
| 104 |
-
auth_config = server_meta.get("auth", {})
|
| 105 |
-
|
| 106 |
-
if auth_config.get("type") == "bearer":
|
| 107 |
-
token = auth_config.get("token")
|
| 108 |
-
if token and token.startswith("${") and token.endswith("}"):
|
| 109 |
-
# Environment variable
|
| 110 |
-
token = os.getenv(token[2:-1], "")
|
| 111 |
-
if token:
|
| 112 |
-
headers["Authorization"] = f"Bearer {token}"
|
| 113 |
-
|
| 114 |
-
elif auth_config.get("type") == "api_key":
|
| 115 |
-
api_key = auth_config.get("key")
|
| 116 |
-
header_name = auth_config.get("header", "X-API-Key")
|
| 117 |
-
if api_key and api_key.startswith("${") and api_key.endswith("}"):
|
| 118 |
-
# Environment variable
|
| 119 |
-
api_key = os.getenv(api_key[2:-1], "")
|
| 120 |
-
if api_key:
|
| 121 |
-
headers[header_name] = api_key
|
| 122 |
-
|
| 123 |
-
elif auth_config.get("type") == "basic":
|
| 124 |
-
username = auth_config.get("username", "")
|
| 125 |
-
password = auth_config.get("password", "")
|
| 126 |
-
if username.startswith("${") and username.endswith("}"):
|
| 127 |
-
username = os.getenv(username[2:-1], "")
|
| 128 |
-
if password.startswith("${") and password.endswith("}"):
|
| 129 |
-
password = os.getenv(password[2:-1], "")
|
| 130 |
-
if username and password:
|
| 131 |
-
import base64
|
| 132 |
-
credentials = base64.b64encode(f"{username}:{password}".encode()).decode()
|
| 133 |
-
headers["Authorization"] = f"Basic {credentials}"
|
| 134 |
-
|
| 135 |
-
# Set up SSL context
|
| 136 |
-
ssl_context = None
|
| 137 |
-
ssl_config = server_meta.get("ssl", {})
|
| 138 |
-
if ssl_config:
|
| 139 |
-
ssl_context = ssl.create_default_context()
|
| 140 |
-
if ssl_config.get("verify_ssl", True) is False:
|
| 141 |
-
ssl_context.check_hostname = False
|
| 142 |
-
ssl_context.verify_mode = ssl.CERT_NONE
|
| 143 |
-
|
| 144 |
-
cert_file = ssl_config.get("cert_file")
|
| 145 |
-
key_file = ssl_config.get("key_file")
|
| 146 |
-
if cert_file and key_file:
|
| 147 |
-
ssl_context.load_cert_chain(cert_file, key_file)
|
| 148 |
-
|
| 149 |
-
ca_file = ssl_config.get("ca_file")
|
| 150 |
-
if ca_file:
|
| 151 |
-
ssl_context.load_verify_locations(ca_file)
|
| 152 |
-
|
| 153 |
-
# Set up timeouts
|
| 154 |
-
timeout_config = server_meta.get("timeout", {})
|
| 155 |
-
timeout = aiohttp.ClientTimeout(
|
| 156 |
-
total=timeout_config.get("total", self.default_timeout),
|
| 157 |
-
connect=timeout_config.get("connect", 10),
|
| 158 |
-
sock_read=timeout_config.get("read", 10)
|
| 159 |
-
)
|
| 160 |
-
|
| 161 |
-
# Create connector with SSL context
|
| 162 |
-
connector = aiohttp.TCPConnector(ssl=ssl_context) if ssl_context else None
|
| 163 |
-
|
| 164 |
-
return aiohttp.ClientSession(
|
| 165 |
-
headers=headers,
|
| 166 |
-
timeout=timeout,
|
| 167 |
-
connector=connector
|
| 168 |
-
)
|
| 169 |
-
|
| 170 |
-
async def _retry_request(self, session: 'aiohttp.ClientSession', method: str, url: str,
|
| 171 |
-
retries: int = None, delay: float = None, **kwargs) -> 'aiohttp.ClientResponse':
|
| 172 |
-
"""Retry HTTP requests with exponential backoff."""
|
| 173 |
-
import aiohttp
|
| 174 |
-
|
| 175 |
-
retries = retries or self.default_retries
|
| 176 |
-
delay = delay or self.retry_delay
|
| 177 |
-
|
| 178 |
-
last_exception = None
|
| 179 |
-
for attempt in range(retries + 1):
|
| 180 |
-
try:
|
| 181 |
-
async with session.request(method, url, **kwargs) as response:
|
| 182 |
-
if response.status < 500: # Don't retry client errors
|
| 183 |
-
return response
|
| 184 |
-
elif attempt == retries:
|
| 185 |
-
response.raise_for_status()
|
| 186 |
-
else:
|
| 187 |
-
self.logger.warning(f"Server error {response.status}, retrying in {delay}s...")
|
| 188 |
-
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
|
| 189 |
-
last_exception = e
|
| 190 |
-
if attempt == retries:
|
| 191 |
-
raise
|
| 192 |
-
self.logger.warning(f"Request failed (attempt {attempt + 1}/{retries + 1}): {e}")
|
| 193 |
-
|
| 194 |
-
if attempt < retries:
|
| 195 |
-
await asyncio.sleep(delay)
|
| 196 |
-
delay *= 2 # Exponential backoff
|
| 197 |
-
|
| 198 |
-
raise last_exception or RuntimeError("Max retries exceeded")
|
| 199 |
-
|
| 200 |
-
async def _health_check(self, server_url: str, session: 'aiohttp.ClientSession') -> bool:
|
| 201 |
-
"""Check if remote MCP server is healthy."""
|
| 202 |
-
try:
|
| 203 |
-
response = await self._retry_request(session, "GET", f"{server_url}/health", retries=1)
|
| 204 |
-
return response.status == 200
|
| 205 |
-
except Exception as e:
|
| 206 |
-
self.logger.debug(f"Health check failed for {server_url}: {e}")
|
| 207 |
-
# Try ping endpoint as fallback
|
| 208 |
-
try:
|
| 209 |
-
response = await self._retry_request(session, "GET", f"{server_url}/ping", retries=1)
|
| 210 |
-
return response.status == 200
|
| 211 |
-
except Exception:
|
| 212 |
-
return False
|
| 213 |
-
|
| 214 |
-
def _add_remote_mcp_server(self, server_name: str, server_meta: dict, tool_registry=None) -> None:
|
| 215 |
-
"""Add tools from a remote MCP server."""
|
| 216 |
-
try:
|
| 217 |
-
import aiohttp
|
| 218 |
-
import json
|
| 219 |
-
except ImportError as e:
|
| 220 |
-
print(f"Warning: aiohttp required for remote MCP servers. Install with: pip install aiohttp")
|
| 221 |
-
return
|
| 222 |
-
|
| 223 |
-
server_url = server_meta["url"].rstrip('/') # Remove trailing slash
|
| 224 |
-
print(f"🌐 Connecting to remote MCP server: {server_name} at {server_url}")
|
| 225 |
-
|
| 226 |
-
# Validate URL format
|
| 227 |
-
parsed_url = urlparse(server_url)
|
| 228 |
-
if not parsed_url.scheme or not parsed_url.netloc:
|
| 229 |
-
print(f"Error: Invalid URL format for server {server_name}: {server_url}")
|
| 230 |
-
return
|
| 231 |
-
|
| 232 |
-
def discover_remote_tools_sync(url: str) -> List[dict]:
|
| 233 |
-
"""Discover available tools from remote MCP server synchronously."""
|
| 234 |
try:
|
| 235 |
async def _discover_async():
|
| 236 |
-
async with
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
self.logger.warning(f"Health check failed for {url}, proceeding anyway...")
|
| 240 |
-
|
| 241 |
-
# Try to discover tools via HTTP API
|
| 242 |
-
try:
|
| 243 |
-
response = await self._retry_request(session, "POST", f"{url}/list_tools", json={})
|
| 244 |
-
if response.status == 200:
|
| 245 |
-
result = await response.json()
|
| 246 |
-
tools = result.get("tools", [])
|
| 247 |
-
|
| 248 |
-
discovered_tools = []
|
| 249 |
-
for tool in tools:
|
| 250 |
-
if isinstance(tool, dict) and "name" in tool:
|
| 251 |
-
description = tool.get('description', '')
|
| 252 |
-
if not description or description.strip() == "":
|
| 253 |
-
formatted_name = tool["name"].replace('_', ' ').title()
|
| 254 |
-
description = f"Remote MCP tool: {formatted_name}"
|
| 255 |
-
|
| 256 |
-
discovered_tools.append({
|
| 257 |
-
"name": tool["name"],
|
| 258 |
-
"description": description,
|
| 259 |
-
"inputSchema": tool.get("inputSchema", {}),
|
| 260 |
-
})
|
| 261 |
-
else:
|
| 262 |
-
print(f"Warning: Skipping invalid tool: {tool}")
|
| 263 |
-
|
| 264 |
-
return discovered_tools
|
| 265 |
-
else:
|
| 266 |
-
await response.text() # Read response body for logging
|
| 267 |
-
print(f"Failed to discover tools from {url}: HTTP {response.status}")
|
| 268 |
-
return []
|
| 269 |
-
except Exception as e:
|
| 270 |
-
print(f"Failed to discover tools from remote server {url}: {e}")
|
| 271 |
-
return []
|
| 272 |
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
return []
|
| 277 |
|
| 278 |
-
|
| 279 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
payload = {
|
| 287 |
-
"name": tool_name,
|
| 288 |
-
"arguments": kwargs
|
| 289 |
-
}
|
| 290 |
-
|
| 291 |
-
# Add connection info tracking
|
| 292 |
-
connection_key = f"{server_name}_{tool_name}"
|
| 293 |
-
self.remote_connections[connection_key] = {
|
| 294 |
-
"server": server_name,
|
| 295 |
-
"tool": tool_name,
|
| 296 |
-
"last_used": asyncio.get_event_loop().time(),
|
| 297 |
-
"status": "active"
|
| 298 |
-
}
|
| 299 |
-
|
| 300 |
-
try:
|
| 301 |
-
response = await self._retry_request(
|
| 302 |
-
session, "POST", f"{url}/call_tool",
|
| 303 |
-
json=payload
|
| 304 |
-
)
|
| 305 |
-
|
| 306 |
-
if response.status == 200:
|
| 307 |
-
result = await response.json()
|
| 308 |
-
self.remote_connections[connection_key]["status"] = "success"
|
| 309 |
-
|
| 310 |
-
# Handle different response formats
|
| 311 |
-
if "content" in result:
|
| 312 |
-
content = result["content"]
|
| 313 |
-
if isinstance(content, list) and len(content) > 0:
|
| 314 |
-
first_content = content[0]
|
| 315 |
-
if isinstance(first_content, dict):
|
| 316 |
-
if "json" in first_content:
|
| 317 |
-
return first_content["json"]
|
| 318 |
-
elif "text" in first_content:
|
| 319 |
-
return first_content["text"]
|
| 320 |
-
return first_content
|
| 321 |
-
return content
|
| 322 |
-
elif "result" in result:
|
| 323 |
-
return result["result"]
|
| 324 |
-
else:
|
| 325 |
-
return result
|
| 326 |
else:
|
| 327 |
-
|
| 328 |
-
self.remote_connections[connection_key]["status"] = "error"
|
| 329 |
-
self.remote_connections[connection_key]["error"] = f"HTTP {response.status}"
|
| 330 |
-
raise RuntimeError(f"Remote MCP call failed: HTTP {response.status} - {error_text}")
|
| 331 |
-
|
| 332 |
-
except Exception as e:
|
| 333 |
-
self.remote_connections[connection_key]["status"] = "error"
|
| 334 |
-
self.remote_connections[connection_key]["error"] = str(e)
|
| 335 |
-
raise
|
| 336 |
-
|
| 337 |
-
try:
|
| 338 |
-
loop = asyncio.get_running_loop()
|
| 339 |
-
return loop.create_task(async_remote_tool_call())
|
| 340 |
-
except RuntimeError:
|
| 341 |
-
return asyncio.run(async_remote_tool_call())
|
| 342 |
-
|
| 343 |
-
except Exception as e:
|
| 344 |
-
self.logger.error(f"Remote MCP tool execution failed for '{tool_name}': {e}")
|
| 345 |
-
raise RuntimeError(f"Remote MCP tool execution failed for '{tool_name}': {e}") from e
|
| 346 |
-
|
| 347 |
-
sync_remote_tool_wrapper.__name__ = tool_name
|
| 348 |
-
sync_remote_tool_wrapper.__doc__ = doc
|
| 349 |
-
return sync_remote_tool_wrapper
|
| 350 |
-
|
| 351 |
-
# Create module namespace for this MCP server
|
| 352 |
-
mcp_module_name = f"mcp_servers.{server_name}"
|
| 353 |
-
if mcp_module_name not in sys.modules:
|
| 354 |
-
sys.modules[mcp_module_name] = types.ModuleType(mcp_module_name)
|
| 355 |
-
server_module = sys.modules[mcp_module_name]
|
| 356 |
-
|
| 357 |
-
tools_config = server_meta.get("tools", [])
|
| 358 |
-
|
| 359 |
-
# Auto-discover tools if not manually configured
|
| 360 |
-
if not tools_config:
|
| 361 |
-
try:
|
| 362 |
-
tools_config = discover_remote_tools_sync(server_url)
|
| 363 |
|
| 364 |
-
|
| 365 |
-
print(f"🔍 Discovered {len(tools_config)} tools from remote {server_name} MCP server")
|
| 366 |
-
else:
|
| 367 |
-
self.logger.warning(f"No tools discovered from remote {server_name} MCP server")
|
| 368 |
-
print(f"Warning: No tools discovered from remote {server_name} MCP server")
|
| 369 |
-
return
|
| 370 |
|
|
|
|
| 371 |
except Exception as e:
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
return
|
| 375 |
-
|
| 376 |
-
# Register each tool
|
| 377 |
-
tools_added = 0
|
| 378 |
-
for tool_meta in tools_config:
|
| 379 |
-
if isinstance(tool_meta, dict) and "biomni_name" in tool_meta:
|
| 380 |
-
# Manual tool definition (Biomni-style)
|
| 381 |
-
tool_name = tool_meta.get("biomni_name")
|
| 382 |
-
description = tool_meta.get("description", f"Remote MCP tool: {tool_name}")
|
| 383 |
-
parameters = tool_meta.get("parameters", {})
|
| 384 |
-
required_param_names = []
|
| 385 |
-
for param_name, param_spec in parameters.items():
|
| 386 |
-
if param_spec.get("required", False):
|
| 387 |
-
required_param_names.append(param_name)
|
| 388 |
-
else:
|
| 389 |
-
# Auto-discovered tool
|
| 390 |
-
tool_name = tool_meta.get("name")
|
| 391 |
-
description = tool_meta.get("description", "")
|
| 392 |
-
|
| 393 |
-
# Ensure description is never empty
|
| 394 |
-
if not description or description.strip() == "":
|
| 395 |
-
formatted_name = tool_name.replace('_', ' ').title()
|
| 396 |
-
description = f"Remote MCP tool: {formatted_name}"
|
| 397 |
-
|
| 398 |
-
input_schema = tool_meta.get("inputSchema", {})
|
| 399 |
-
parameters = input_schema.get("properties", {})
|
| 400 |
-
required_param_names = input_schema.get("required", [])
|
| 401 |
-
|
| 402 |
-
if not tool_name:
|
| 403 |
-
self.logger.warning(f"Skipping tool with no name in remote {server_name}")
|
| 404 |
-
print(f"Warning: Skipping tool with no name in remote {server_name}")
|
| 405 |
-
continue
|
| 406 |
-
|
| 407 |
-
# Create wrapper function for remote tool
|
| 408 |
-
wrapper_function = make_remote_mcp_wrapper(server_url, tool_name, description)
|
| 409 |
-
|
| 410 |
-
# Add to module namespace
|
| 411 |
-
setattr(server_module, tool_name, wrapper_function)
|
| 412 |
-
|
| 413 |
-
# Store in MCP functions registry with parameter information
|
| 414 |
-
self.mcp_functions[tool_name] = {
|
| 415 |
-
"function": wrapper_function,
|
| 416 |
-
"server": server_name,
|
| 417 |
-
"module": mcp_module_name,
|
| 418 |
-
"description": description,
|
| 419 |
-
"server_type": "remote",
|
| 420 |
-
"server_url": server_url,
|
| 421 |
-
"required_parameters": [], # Will be populated below
|
| 422 |
-
"optional_parameters": [] # Will be populated below
|
| 423 |
-
}
|
| 424 |
-
|
| 425 |
-
# Register with tool registry if available
|
| 426 |
-
if tool_registry:
|
| 427 |
-
from .tool_registry import ToolRegistry
|
| 428 |
-
# Create tool schema with proper parameter information
|
| 429 |
-
required_params = []
|
| 430 |
-
optional_params = []
|
| 431 |
-
|
| 432 |
-
for param_name, param_spec in parameters.items():
|
| 433 |
-
param_info = {
|
| 434 |
-
"name": param_name,
|
| 435 |
-
"type": param_spec.get("type", "string"),
|
| 436 |
-
"description": param_spec.get("description", f"Parameter {param_name}"),
|
| 437 |
-
}
|
| 438 |
-
|
| 439 |
-
# Extract enum/literal values if present
|
| 440 |
-
if "enum" in param_spec:
|
| 441 |
-
param_info["enum"] = param_spec["enum"]
|
| 442 |
-
|
| 443 |
-
# Handle anyOf schemas (common for optional literal types)
|
| 444 |
-
if "anyOf" in param_spec:
|
| 445 |
-
for schema_option in param_spec["anyOf"]:
|
| 446 |
-
if "enum" in schema_option:
|
| 447 |
-
param_info["enum"] = schema_option["enum"]
|
| 448 |
-
if "type" in schema_option:
|
| 449 |
-
param_info["type"] = schema_option["type"]
|
| 450 |
-
break
|
| 451 |
-
|
| 452 |
-
# Handle oneOf schemas (alternative union syntax)
|
| 453 |
-
if "oneOf" in param_spec:
|
| 454 |
-
for schema_option in param_spec["oneOf"]:
|
| 455 |
-
if "enum" in schema_option:
|
| 456 |
-
param_info["enum"] = schema_option["enum"]
|
| 457 |
-
if "type" in schema_option:
|
| 458 |
-
param_info["type"] = schema_option["type"]
|
| 459 |
-
break
|
| 460 |
-
|
| 461 |
-
# Determine if parameter is required
|
| 462 |
-
is_required = (param_name in required_param_names) or ("default" not in param_spec)
|
| 463 |
-
|
| 464 |
-
if is_required:
|
| 465 |
-
required_params.append(param_info)
|
| 466 |
-
else:
|
| 467 |
-
param_info["default"] = param_spec.get("default")
|
| 468 |
-
optional_params.append(param_info)
|
| 469 |
-
|
| 470 |
-
# Create complete tool schema
|
| 471 |
-
tool_schema = {
|
| 472 |
-
"name": tool_name,
|
| 473 |
-
"description": description,
|
| 474 |
-
"required_parameters": required_params,
|
| 475 |
-
"optional_parameters": optional_params,
|
| 476 |
-
"module": mcp_module_name,
|
| 477 |
-
}
|
| 478 |
-
|
| 479 |
-
success = tool_registry.register_tool(tool_schema, mcp_module_name)
|
| 480 |
-
if success:
|
| 481 |
-
tool_registry._name_to_function[tool_name] = wrapper_function
|
| 482 |
-
tools_added += 1
|
| 483 |
-
|
| 484 |
-
# Update MCP functions registry with parameter information
|
| 485 |
-
self.mcp_functions[tool_name]["required_parameters"] = required_params
|
| 486 |
-
self.mcp_functions[tool_name]["optional_parameters"] = optional_params
|
| 487 |
-
|
| 488 |
-
if tools_added > 0:
|
| 489 |
-
print(f"✅ Added {tools_added} remote MCP tools from {server_name} server")
|
| 490 |
-
# Store server connection info
|
| 491 |
-
try:
|
| 492 |
-
current_time = asyncio.get_event_loop().time()
|
| 493 |
-
except RuntimeError:
|
| 494 |
-
current_time = 0
|
| 495 |
-
|
| 496 |
-
self.remote_connections[f"{server_name}_server"] = {
|
| 497 |
-
"server": server_name,
|
| 498 |
-
"url": server_url,
|
| 499 |
-
"type": "http",
|
| 500 |
-
"status": "connected",
|
| 501 |
-
"tools_count": tools_added,
|
| 502 |
-
"connected_at": current_time
|
| 503 |
-
}
|
| 504 |
-
|
| 505 |
-
def _add_local_mcp_server(self, server_name: str, server_meta: dict, tool_registry=None) -> None:
|
| 506 |
-
"""Add tools from a local MCP server (stdio)."""
|
| 507 |
-
try:
|
| 508 |
-
from mcp.client.stdio import StdioServerParameters, stdio_client
|
| 509 |
-
from mcp import ClientSession
|
| 510 |
-
except ImportError as e:
|
| 511 |
-
print(f"Warning: MCP client libraries required for local MCP servers. Install with: pip install mcp")
|
| 512 |
-
return
|
| 513 |
-
|
| 514 |
-
cmd_list = server_meta.get("command", [])
|
| 515 |
-
cmd, *args = cmd_list
|
| 516 |
-
|
| 517 |
-
# Process environment variables
|
| 518 |
-
env_vars = server_meta.get("env", {})
|
| 519 |
-
if env_vars:
|
| 520 |
-
processed_env = {}
|
| 521 |
-
for key, value in env_vars.items():
|
| 522 |
-
if isinstance(value, str) and value.startswith("${") and value.endswith("}"):
|
| 523 |
-
var_name = value[2:-1]
|
| 524 |
-
processed_env[key] = os.getenv(var_name, "")
|
| 525 |
-
else:
|
| 526 |
-
processed_env[key] = value
|
| 527 |
-
env_vars = processed_env
|
| 528 |
-
|
| 529 |
-
print(f"💻 Connecting to local MCP server: {server_name}")
|
| 530 |
|
| 531 |
-
def
|
| 532 |
-
"""Discover available tools from
|
| 533 |
try:
|
| 534 |
-
async def
|
| 535 |
-
async with
|
| 536 |
-
async with ClientSession(
|
| 537 |
await session.initialize()
|
| 538 |
-
|
| 539 |
-
tools_result = await session.list_tools()
|
| 540 |
-
tools = tools_result.tools if hasattr(tools_result, "tools") else tools_result
|
| 541 |
|
| 542 |
discovered_tools = []
|
| 543 |
for tool in tools:
|
| 544 |
if hasattr(tool, "name"):
|
|
|
|
| 545 |
description = getattr(tool, 'description', None)
|
| 546 |
if not description or description.strip() == "":
|
|
|
|
| 547 |
formatted_name = tool.name.replace('_', ' ').title()
|
| 548 |
-
description = f"
|
| 549 |
|
| 550 |
discovered_tools.append({
|
| 551 |
"name": tool.name,
|
|
@@ -557,16 +119,16 @@ class MCPManager:
|
|
| 557 |
|
| 558 |
return discovered_tools
|
| 559 |
|
| 560 |
-
return asyncio.run(
|
| 561 |
except Exception as e:
|
| 562 |
-
print(f"Failed to discover
|
| 563 |
return []
|
| 564 |
|
| 565 |
-
def
|
| 566 |
-
"""Create a synchronous wrapper for
|
| 567 |
|
| 568 |
def sync_tool_wrapper(**kwargs):
|
| 569 |
-
"""Synchronous wrapper for
|
| 570 |
try:
|
| 571 |
server_params = StdioServerParameters(command=cmd, args=args, env=env_vars)
|
| 572 |
|
|
@@ -576,7 +138,40 @@ class MCPManager:
|
|
| 576 |
await session.initialize()
|
| 577 |
result = await session.call_tool(tool_name, kwargs)
|
| 578 |
content = result.content[0]
|
| 579 |
-
if hasattr(content, "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 580 |
return content.json()
|
| 581 |
return content.text
|
| 582 |
|
|
@@ -587,13 +182,208 @@ class MCPManager:
|
|
| 587 |
return asyncio.run(async_tool_call())
|
| 588 |
|
| 589 |
except Exception as e:
|
| 590 |
-
raise RuntimeError(f"
|
| 591 |
|
| 592 |
sync_tool_wrapper.__name__ = tool_name
|
| 593 |
sync_tool_wrapper.__doc__ = doc
|
| 594 |
return sync_tool_wrapper
|
| 595 |
|
| 596 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 597 |
mcp_module_name = f"mcp_servers.{server_name}"
|
| 598 |
if mcp_module_name not in sys.modules:
|
| 599 |
sys.modules[mcp_module_name] = types.ModuleType(mcp_module_name)
|
|
@@ -604,17 +394,16 @@ class MCPManager:
|
|
| 604 |
# Auto-discover tools if not manually configured
|
| 605 |
if not tools_config:
|
| 606 |
try:
|
| 607 |
-
|
| 608 |
-
tools_config = discover_local_tools_sync(server_params)
|
| 609 |
|
| 610 |
if tools_config:
|
| 611 |
-
print(f"🔍 Discovered {len(tools_config)} tools from
|
| 612 |
else:
|
| 613 |
-
print(f"Warning: No tools discovered from
|
| 614 |
return
|
| 615 |
|
| 616 |
except Exception as e:
|
| 617 |
-
print(f"Failed to discover tools for
|
| 618 |
return
|
| 619 |
|
| 620 |
# Register each tool
|
|
@@ -623,7 +412,7 @@ class MCPManager:
|
|
| 623 |
if isinstance(tool_meta, dict) and "biomni_name" in tool_meta:
|
| 624 |
# Manual tool definition (Biomni-style)
|
| 625 |
tool_name = tool_meta.get("biomni_name")
|
| 626 |
-
description = tool_meta.get("description", f"
|
| 627 |
parameters = tool_meta.get("parameters", {})
|
| 628 |
required_param_names = []
|
| 629 |
for param_name, param_spec in parameters.items():
|
|
@@ -637,18 +426,18 @@ class MCPManager:
|
|
| 637 |
# Ensure description is never empty
|
| 638 |
if not description or description.strip() == "":
|
| 639 |
formatted_name = tool_name.replace('_', ' ').title()
|
| 640 |
-
description = f"
|
| 641 |
|
| 642 |
input_schema = tool_meta.get("inputSchema", {})
|
| 643 |
parameters = input_schema.get("properties", {})
|
| 644 |
required_param_names = input_schema.get("required", [])
|
| 645 |
|
| 646 |
if not tool_name:
|
| 647 |
-
print(f"Warning: Skipping tool with no name in
|
| 648 |
continue
|
| 649 |
|
| 650 |
-
# Create wrapper function
|
| 651 |
-
wrapper_function =
|
| 652 |
|
| 653 |
# Add to module namespace
|
| 654 |
setattr(server_module, tool_name, wrapper_function)
|
|
@@ -659,9 +448,9 @@ class MCPManager:
|
|
| 659 |
"server": server_name,
|
| 660 |
"module": mcp_module_name,
|
| 661 |
"description": description,
|
| 662 |
-
"server_type": "local",
|
| 663 |
"required_parameters": [], # Will be populated below
|
| 664 |
-
"optional_parameters": []
|
|
|
|
| 665 |
}
|
| 666 |
|
| 667 |
# Register with tool registry if available
|
|
@@ -684,15 +473,18 @@ class MCPManager:
|
|
| 684 |
|
| 685 |
# Handle anyOf schemas (common for optional literal types)
|
| 686 |
if "anyOf" in param_spec:
|
|
|
|
| 687 |
for schema_option in param_spec["anyOf"]:
|
| 688 |
if "enum" in schema_option:
|
| 689 |
param_info["enum"] = schema_option["enum"]
|
|
|
|
| 690 |
if "type" in schema_option:
|
| 691 |
param_info["type"] = schema_option["type"]
|
| 692 |
break
|
| 693 |
|
| 694 |
# Handle oneOf schemas (alternative union syntax)
|
| 695 |
if "oneOf" in param_spec:
|
|
|
|
| 696 |
for schema_option in param_spec["oneOf"]:
|
| 697 |
if "enum" in schema_option:
|
| 698 |
param_info["enum"] = schema_option["enum"]
|
|
@@ -700,7 +492,9 @@ class MCPManager:
|
|
| 700 |
param_info["type"] = schema_option["type"]
|
| 701 |
break
|
| 702 |
|
| 703 |
-
# Determine if parameter is required
|
|
|
|
|
|
|
| 704 |
is_required = (param_name in required_param_names) or ("default" not in param_spec)
|
| 705 |
|
| 706 |
if is_required:
|
|
@@ -728,7 +522,7 @@ class MCPManager:
|
|
| 728 |
self.mcp_functions[tool_name]["optional_parameters"] = optional_params
|
| 729 |
|
| 730 |
if tools_added > 0:
|
| 731 |
-
print(f"✅ Added {tools_added}
|
| 732 |
|
| 733 |
def list_mcp_tools(self) -> Dict[str, dict]:
|
| 734 |
"""List all loaded MCP tools."""
|
|
@@ -747,70 +541,6 @@ class MCPManager:
|
|
| 747 |
del self.mcp_functions[tool_name]
|
| 748 |
return True
|
| 749 |
|
| 750 |
-
async def _create_websocket_connection(self, server_url: str, server_meta: dict) -> Optional[Any]:
|
| 751 |
-
"""Create WebSocket connection for real-time MCP communication."""
|
| 752 |
-
try:
|
| 753 |
-
import aiohttp
|
| 754 |
-
|
| 755 |
-
# Convert HTTP URL to WebSocket URL
|
| 756 |
-
ws_url = server_url.replace('http://', 'ws://').replace('https://', 'wss://')
|
| 757 |
-
if not ws_url.endswith('/ws'):
|
| 758 |
-
ws_url += '/ws'
|
| 759 |
-
|
| 760 |
-
session = await self._create_session_with_auth(server_meta)
|
| 761 |
-
ws = await session.ws_connect(ws_url)
|
| 762 |
-
|
| 763 |
-
self.logger.info(f"WebSocket connection established to {ws_url}")
|
| 764 |
-
return ws
|
| 765 |
-
|
| 766 |
-
except Exception as e:
|
| 767 |
-
self.logger.debug(f"WebSocket connection failed: {e}")
|
| 768 |
-
return None
|
| 769 |
-
|
| 770 |
-
def get_connection_status(self) -> Dict[str, Any]:
|
| 771 |
-
"""Get status of all remote connections."""
|
| 772 |
-
return {
|
| 773 |
-
"connections": self.remote_connections.copy(),
|
| 774 |
-
"total_connections": len(self.remote_connections),
|
| 775 |
-
"active_connections": len([c for c in self.remote_connections.values()
|
| 776 |
-
if c.get("status") in ["connected", "active", "success"]])
|
| 777 |
-
}
|
| 778 |
-
|
| 779 |
-
async def test_remote_connection(self, server_name: str) -> Dict[str, Any]:
|
| 780 |
-
"""Test connection to a remote MCP server."""
|
| 781 |
-
server_info = None
|
| 782 |
-
for name, info in self.remote_connections.items():
|
| 783 |
-
if info.get("server") == server_name and "_server" in name:
|
| 784 |
-
server_info = info
|
| 785 |
-
break
|
| 786 |
-
|
| 787 |
-
if not server_info:
|
| 788 |
-
return {"status": "error", "message": f"Server {server_name} not found"}
|
| 789 |
-
|
| 790 |
-
try:
|
| 791 |
-
import aiohttp
|
| 792 |
-
server_url = server_info["url"]
|
| 793 |
-
|
| 794 |
-
# Find server metadata from current config
|
| 795 |
-
server_meta = {}
|
| 796 |
-
# This would need to be passed or stored - simplified for now
|
| 797 |
-
|
| 798 |
-
async with await self._create_session_with_auth(server_meta) as session:
|
| 799 |
-
is_healthy = await self._health_check(server_url, session)
|
| 800 |
-
|
| 801 |
-
return {
|
| 802 |
-
"status": "healthy" if is_healthy else "unhealthy",
|
| 803 |
-
"server": server_name,
|
| 804 |
-
"url": server_url,
|
| 805 |
-
"timestamp": asyncio.get_event_loop().time()
|
| 806 |
-
}
|
| 807 |
-
except Exception as e:
|
| 808 |
-
return {
|
| 809 |
-
"status": "error",
|
| 810 |
-
"server": server_name,
|
| 811 |
-
"error": str(e)
|
| 812 |
-
}
|
| 813 |
-
|
| 814 |
def show_mcp_status(self) -> None:
|
| 815 |
"""Display detailed MCP status information to the user."""
|
| 816 |
if not self.has_mcp_functions():
|
|
@@ -830,30 +560,9 @@ class MCPManager:
|
|
| 830 |
self.console.print(f" 📊 Total servers: {len(servers)}")
|
| 831 |
self.console.print(f" 🛠️ Total MCP tools: {len(mcp_tools)}")
|
| 832 |
|
| 833 |
-
# Show connection status
|
| 834 |
-
connection_status = self.get_connection_status()
|
| 835 |
-
if connection_status["total_connections"] > 0:
|
| 836 |
-
self.console.print(f" 🌐 Remote connections: {connection_status['active_connections']}/{connection_status['total_connections']} active")
|
| 837 |
-
|
| 838 |
for server_name, tools in servers.items():
|
| 839 |
-
|
| 840 |
-
server_type = "unknown"
|
| 841 |
-
for tool_name, tool_info in mcp_tools.items():
|
| 842 |
-
if tool_info.get('server') == server_name:
|
| 843 |
-
server_type = tool_info.get('server_type', 'unknown')
|
| 844 |
-
break
|
| 845 |
-
|
| 846 |
-
status_icon = "🌐" if server_type == "remote" else "💻"
|
| 847 |
-
self.console.print(f"\n {status_icon} Server: {server_name} ({server_type})")
|
| 848 |
self.console.print(f" Status: ✅ Active ({len(tools)} tools)")
|
| 849 |
-
|
| 850 |
-
# Show connection details for remote servers
|
| 851 |
-
if server_type == "remote":
|
| 852 |
-
for conn_name, conn_info in self.remote_connections.items():
|
| 853 |
-
if conn_info.get("server") == server_name and "_server" in conn_name:
|
| 854 |
-
self.console.print(f" URL: {conn_info.get('url', 'Unknown')}")
|
| 855 |
-
break
|
| 856 |
-
|
| 857 |
for tool in tools:
|
| 858 |
self.console.print(f" • {tool['name']}: {tool['description']}")
|
| 859 |
|
|
@@ -875,6 +584,5 @@ class MCPManager:
|
|
| 875 |
"total_tools": len(mcp_tools),
|
| 876 |
"total_servers": len(servers),
|
| 877 |
"servers": servers,
|
| 878 |
-
"tools": {name: info.get('description', '') for name, info in mcp_tools.items()}
|
| 879 |
-
"connections": self.get_connection_status()
|
| 880 |
}
|
|
|
|
| 1 |
"""
|
| 2 |
MCP Manager for CodeAct Agent.
|
| 3 |
Manages MCP (Model Context Protocol) tools and servers.
|
|
|
|
| 4 |
"""
|
| 5 |
|
| 6 |
import os
|
| 7 |
import sys
|
|
|
|
| 8 |
import types
|
|
|
|
|
|
|
| 9 |
from pathlib import Path
|
| 10 |
+
from typing import Dict, List, Optional, Any
|
| 11 |
from rich.console import Console
|
|
|
|
| 12 |
|
| 13 |
|
| 14 |
class MCPManager:
|
|
|
|
| 17 |
def __init__(self, console_display=None):
|
| 18 |
self.mcp_functions = {}
|
| 19 |
self.console = console_display.console if console_display else Console()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
def has_mcp_functions(self) -> bool:
|
| 22 |
"""Check if MCP functions are available."""
|
|
|
|
| 36 |
return servers
|
| 37 |
|
| 38 |
def add_mcp(self, config_path: str = "./mcp_config.yaml", tool_registry=None) -> None:
|
| 39 |
+
"""Add MCP tools from configuration file."""
|
| 40 |
try:
|
| 41 |
+
import asyncio
|
| 42 |
import yaml
|
| 43 |
except ImportError as e:
|
| 44 |
raise ImportError(f"Required packages not available: {e}. Install with: pip install pyyaml") from e
|
| 45 |
|
| 46 |
try:
|
| 47 |
import nest_asyncio
|
| 48 |
+
from mcp import ClientSession
|
| 49 |
+
from mcp.client.stdio import StdioServerParameters, stdio_client
|
| 50 |
+
from mcp.client.streamable_http import streamablehttp_client
|
| 51 |
+
from langchain_mcp_adapters.tools import _list_all_tools
|
| 52 |
nest_asyncio.apply()
|
| 53 |
except ImportError as e:
|
| 54 |
+
raise ImportError(f"MCP packages not available: {e}. Install with: pip install mcp langchain-mcp-adapters") from e
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
+
def discover_mcp_tools_sync(server_params: StdioServerParameters) -> List[dict]:
|
| 57 |
+
"""Discover available tools from MCP server synchronously."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
try:
|
| 59 |
async def _discover_async():
|
| 60 |
+
async with stdio_client(server_params) as (reader, writer):
|
| 61 |
+
async with ClientSession(reader, writer) as session:
|
| 62 |
+
await session.initialize()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
+
tools_result = await session.list_tools()
|
| 65 |
+
tools = tools_result.tools if hasattr(tools_result, "tools") else tools_result
|
| 66 |
+
print(tools)
|
|
|
|
| 67 |
|
| 68 |
+
discovered_tools = []
|
| 69 |
+
for tool in tools:
|
| 70 |
+
if hasattr(tool, "name"):
|
| 71 |
+
# Ensure description is never empty or None
|
| 72 |
+
description = getattr(tool, 'description', None)
|
| 73 |
+
if not description or description.strip() == "":
|
| 74 |
+
# Generate description from tool name
|
| 75 |
+
formatted_name = tool.name.replace('_', ' ').title()
|
| 76 |
+
description = f"MCP tool: {formatted_name}"
|
| 77 |
|
| 78 |
+
discovered_tools.append({
|
| 79 |
+
"name": tool.name,
|
| 80 |
+
"description": description,
|
| 81 |
+
"inputSchema": tool.inputSchema,
|
| 82 |
+
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
else:
|
| 84 |
+
print(f"Warning: Skipping tool with no name attribute: {tool}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
+
return discovered_tools
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
+
return asyncio.run(_discover_async())
|
| 89 |
except Exception as e:
|
| 90 |
+
print(f"Failed to discover tools: {e}")
|
| 91 |
+
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
+
def discover_remote_mcp_tools_sync(url: str) -> List[dict]:
|
| 94 |
+
"""Discover available tools from remote MCP server synchronously."""
|
| 95 |
try:
|
| 96 |
+
async def _discover_remote_async():
|
| 97 |
+
async with streamablehttp_client(url) as (read, write, _):
|
| 98 |
+
async with ClientSession(read, write) as session:
|
| 99 |
await session.initialize()
|
| 100 |
+
tools = await _list_all_tools(session)
|
|
|
|
|
|
|
| 101 |
|
| 102 |
discovered_tools = []
|
| 103 |
for tool in tools:
|
| 104 |
if hasattr(tool, "name"):
|
| 105 |
+
# Ensure description is never empty or None
|
| 106 |
description = getattr(tool, 'description', None)
|
| 107 |
if not description or description.strip() == "":
|
| 108 |
+
# Generate description from tool name
|
| 109 |
formatted_name = tool.name.replace('_', ' ').title()
|
| 110 |
+
description = f"MCP tool: {formatted_name}"
|
| 111 |
|
| 112 |
discovered_tools.append({
|
| 113 |
"name": tool.name,
|
|
|
|
| 119 |
|
| 120 |
return discovered_tools
|
| 121 |
|
| 122 |
+
return asyncio.run(_discover_remote_async())
|
| 123 |
except Exception as e:
|
| 124 |
+
print(f"Failed to discover remote tools from {url}: {e}")
|
| 125 |
return []
|
| 126 |
|
| 127 |
+
def make_mcp_wrapper(cmd: str, args: List[str], tool_name: str, doc: str, env_vars: dict = None):
|
| 128 |
+
"""Create a synchronous wrapper for an async MCP tool call."""
|
| 129 |
|
| 130 |
def sync_tool_wrapper(**kwargs):
|
| 131 |
+
"""Synchronous wrapper for MCP tool execution."""
|
| 132 |
try:
|
| 133 |
server_params = StdioServerParameters(command=cmd, args=args, env=env_vars)
|
| 134 |
|
|
|
|
| 138 |
await session.initialize()
|
| 139 |
result = await session.call_tool(tool_name, kwargs)
|
| 140 |
content = result.content[0]
|
| 141 |
+
if hasattr(content, "model_dump_json"):
|
| 142 |
+
return content.model_dump_json()
|
| 143 |
+
elif hasattr(content, "json"):
|
| 144 |
+
return content.json()
|
| 145 |
+
return content.text
|
| 146 |
+
|
| 147 |
+
try:
|
| 148 |
+
loop = asyncio.get_running_loop()
|
| 149 |
+
return loop.create_task(async_tool_call())
|
| 150 |
+
except RuntimeError:
|
| 151 |
+
return asyncio.run(async_tool_call())
|
| 152 |
+
|
| 153 |
+
except Exception as e:
|
| 154 |
+
raise RuntimeError(f"MCP tool execution failed for '{tool_name}': {e}") from e
|
| 155 |
+
|
| 156 |
+
sync_tool_wrapper.__name__ = tool_name
|
| 157 |
+
sync_tool_wrapper.__doc__ = doc
|
| 158 |
+
return sync_tool_wrapper
|
| 159 |
+
|
| 160 |
+
def make_remote_mcp_wrapper(url: str, tool_name: str, doc: str):
|
| 161 |
+
"""Create a synchronous wrapper for an async remote MCP tool call."""
|
| 162 |
+
|
| 163 |
+
def sync_tool_wrapper(**kwargs):
|
| 164 |
+
"""Synchronous wrapper for remote MCP tool execution."""
|
| 165 |
+
try:
|
| 166 |
+
async def async_tool_call():
|
| 167 |
+
async with streamablehttp_client(url) as (read, write, _):
|
| 168 |
+
async with ClientSession(read, write) as session:
|
| 169 |
+
await session.initialize()
|
| 170 |
+
result = await session.call_tool(tool_name, kwargs)
|
| 171 |
+
content = result.content[0]
|
| 172 |
+
if hasattr(content, "model_dump_json"):
|
| 173 |
+
return content.model_dump_json()
|
| 174 |
+
elif hasattr(content, "json"):
|
| 175 |
return content.json()
|
| 176 |
return content.text
|
| 177 |
|
|
|
|
| 182 |
return asyncio.run(async_tool_call())
|
| 183 |
|
| 184 |
except Exception as e:
|
| 185 |
+
raise RuntimeError(f"Remote MCP tool execution failed for '{tool_name}': {e}") from e
|
| 186 |
|
| 187 |
sync_tool_wrapper.__name__ = tool_name
|
| 188 |
sync_tool_wrapper.__doc__ = doc
|
| 189 |
return sync_tool_wrapper
|
| 190 |
|
| 191 |
+
# Load and validate configuration
|
| 192 |
+
try:
|
| 193 |
+
config_content = Path(config_path).read_text(encoding="utf-8")
|
| 194 |
+
cfg = yaml.safe_load(config_content) or {}
|
| 195 |
+
except FileNotFoundError:
|
| 196 |
+
raise FileNotFoundError(f"MCP config file not found: {config_path}") from None
|
| 197 |
+
except yaml.YAMLError as e:
|
| 198 |
+
raise yaml.YAMLError(f"Invalid YAML in MCP config: {e}") from e
|
| 199 |
+
|
| 200 |
+
mcp_servers = cfg.get("mcp_servers", {})
|
| 201 |
+
if not mcp_servers:
|
| 202 |
+
print("Warning: No MCP servers found in configuration")
|
| 203 |
+
return
|
| 204 |
+
|
| 205 |
+
# Process each MCP server configuration
|
| 206 |
+
for server_name, server_meta in mcp_servers.items():
|
| 207 |
+
if not server_meta.get("enabled", True):
|
| 208 |
+
continue
|
| 209 |
+
|
| 210 |
+
# Check if this is a remote server configuration
|
| 211 |
+
remote_url = server_meta.get("url")
|
| 212 |
+
if remote_url:
|
| 213 |
+
# Handle remote MCP server
|
| 214 |
+
self._process_remote_server(server_name, server_meta, remote_url, tool_registry, discover_remote_mcp_tools_sync, make_remote_mcp_wrapper)
|
| 215 |
+
continue
|
| 216 |
+
|
| 217 |
+
# Handle local MCP server (existing logic)
|
| 218 |
+
# Validate command configuration
|
| 219 |
+
cmd_list = server_meta.get("command", [])
|
| 220 |
+
if not cmd_list or not isinstance(cmd_list, list):
|
| 221 |
+
print(f"Warning: Invalid command configuration for server '{server_name}'")
|
| 222 |
+
continue
|
| 223 |
+
|
| 224 |
+
cmd, *args = cmd_list
|
| 225 |
+
|
| 226 |
+
# Process environment variables
|
| 227 |
+
env_vars = server_meta.get("env", {})
|
| 228 |
+
if env_vars:
|
| 229 |
+
processed_env = {}
|
| 230 |
+
for key, value in env_vars.items():
|
| 231 |
+
if isinstance(value, str) and value.startswith("${") and value.endswith("}"):
|
| 232 |
+
var_name = value[2:-1]
|
| 233 |
+
processed_env[key] = os.getenv(var_name, "")
|
| 234 |
+
else:
|
| 235 |
+
processed_env[key] = value
|
| 236 |
+
env_vars = processed_env
|
| 237 |
+
|
| 238 |
+
# Create module namespace for this MCP server
|
| 239 |
+
mcp_module_name = f"mcp_servers.{server_name}"
|
| 240 |
+
if mcp_module_name not in sys.modules:
|
| 241 |
+
sys.modules[mcp_module_name] = types.ModuleType(mcp_module_name)
|
| 242 |
+
server_module = sys.modules[mcp_module_name]
|
| 243 |
+
|
| 244 |
+
tools_config = server_meta.get("tools", [])
|
| 245 |
+
|
| 246 |
+
# Auto-discover tools if not manually configured
|
| 247 |
+
if not tools_config:
|
| 248 |
+
try:
|
| 249 |
+
server_params = StdioServerParameters(command=cmd, args=args, env=env_vars)
|
| 250 |
+
tools_config = discover_mcp_tools_sync(server_params)
|
| 251 |
+
|
| 252 |
+
if tools_config:
|
| 253 |
+
print(f"🔍 Discovered {len(tools_config)} tools from {server_name} MCP server")
|
| 254 |
+
else:
|
| 255 |
+
print(f"Warning: No tools discovered from {server_name} MCP server")
|
| 256 |
+
continue
|
| 257 |
+
|
| 258 |
+
except Exception as e:
|
| 259 |
+
print(f"Failed to discover tools for {server_name}: {e}")
|
| 260 |
+
continue
|
| 261 |
+
|
| 262 |
+
# Register each tool
|
| 263 |
+
tools_added = 0
|
| 264 |
+
for tool_meta in tools_config:
|
| 265 |
+
if isinstance(tool_meta, dict) and "biomni_name" in tool_meta:
|
| 266 |
+
# Manual tool definition (Biomni-style)
|
| 267 |
+
tool_name = tool_meta.get("biomni_name")
|
| 268 |
+
description = tool_meta.get("description", f"MCP tool: {tool_name}")
|
| 269 |
+
parameters = tool_meta.get("parameters", {})
|
| 270 |
+
required_param_names = []
|
| 271 |
+
for param_name, param_spec in parameters.items():
|
| 272 |
+
if param_spec.get("required", False):
|
| 273 |
+
required_param_names.append(param_name)
|
| 274 |
+
else:
|
| 275 |
+
# Auto-discovered tool
|
| 276 |
+
tool_name = tool_meta.get("name")
|
| 277 |
+
description = tool_meta.get("description", "")
|
| 278 |
+
|
| 279 |
+
# Ensure description is never empty
|
| 280 |
+
if not description or description.strip() == "":
|
| 281 |
+
formatted_name = tool_name.replace('_', ' ').title()
|
| 282 |
+
description = f"MCP tool: {formatted_name}"
|
| 283 |
+
|
| 284 |
+
input_schema = tool_meta.get("inputSchema", {})
|
| 285 |
+
parameters = input_schema.get("properties", {})
|
| 286 |
+
required_param_names = input_schema.get("required", [])
|
| 287 |
+
|
| 288 |
+
if not tool_name:
|
| 289 |
+
print(f"Warning: Skipping tool with no name in {server_name}")
|
| 290 |
+
continue
|
| 291 |
+
|
| 292 |
+
# Create wrapper function
|
| 293 |
+
wrapper_function = make_mcp_wrapper(cmd, args, tool_name, description, env_vars)
|
| 294 |
+
|
| 295 |
+
# Add to module namespace
|
| 296 |
+
setattr(server_module, tool_name, wrapper_function)
|
| 297 |
+
|
| 298 |
+
# Store in MCP functions registry with parameter information
|
| 299 |
+
self.mcp_functions[tool_name] = {
|
| 300 |
+
"function": wrapper_function,
|
| 301 |
+
"server": server_name,
|
| 302 |
+
"module": mcp_module_name,
|
| 303 |
+
"description": description,
|
| 304 |
+
"required_parameters": [], # Will be populated below
|
| 305 |
+
"optional_parameters": [] # Will be populated below
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
# Register with tool registry if available
|
| 309 |
+
if tool_registry:
|
| 310 |
+
from .tool_registry import ToolRegistry
|
| 311 |
+
# Create tool schema with proper parameter information
|
| 312 |
+
required_params = []
|
| 313 |
+
optional_params = []
|
| 314 |
+
|
| 315 |
+
for param_name, param_spec in parameters.items():
|
| 316 |
+
param_info = {
|
| 317 |
+
"name": param_name,
|
| 318 |
+
"type": param_spec.get("type", "string"),
|
| 319 |
+
"description": param_spec.get("description", f"Parameter {param_name}"),
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
# Extract enum/literal values if present
|
| 323 |
+
if "enum" in param_spec:
|
| 324 |
+
param_info["enum"] = param_spec["enum"]
|
| 325 |
+
|
| 326 |
+
# Handle anyOf schemas (common for optional literal types)
|
| 327 |
+
if "anyOf" in param_spec:
|
| 328 |
+
# Look for enum in anyOf schemas
|
| 329 |
+
for schema_option in param_spec["anyOf"]:
|
| 330 |
+
if "enum" in schema_option:
|
| 331 |
+
param_info["enum"] = schema_option["enum"]
|
| 332 |
+
# Update type if specified
|
| 333 |
+
if "type" in schema_option:
|
| 334 |
+
param_info["type"] = schema_option["type"]
|
| 335 |
+
break
|
| 336 |
+
|
| 337 |
+
# Handle oneOf schemas (alternative union syntax)
|
| 338 |
+
if "oneOf" in param_spec:
|
| 339 |
+
# Look for enum in oneOf schemas
|
| 340 |
+
for schema_option in param_spec["oneOf"]:
|
| 341 |
+
if "enum" in schema_option:
|
| 342 |
+
param_info["enum"] = schema_option["enum"]
|
| 343 |
+
if "type" in schema_option:
|
| 344 |
+
param_info["type"] = schema_option["type"]
|
| 345 |
+
break
|
| 346 |
+
|
| 347 |
+
# Determine if parameter is required based on:
|
| 348 |
+
# 1. Explicit required list (if provided)
|
| 349 |
+
# 2. If no default value is present in the schema
|
| 350 |
+
is_required = (param_name in required_param_names) or ("default" not in param_spec)
|
| 351 |
+
|
| 352 |
+
if is_required:
|
| 353 |
+
required_params.append(param_info)
|
| 354 |
+
else:
|
| 355 |
+
param_info["default"] = param_spec.get("default")
|
| 356 |
+
optional_params.append(param_info)
|
| 357 |
+
|
| 358 |
+
# Create complete tool schema
|
| 359 |
+
tool_schema = {
|
| 360 |
+
"name": tool_name,
|
| 361 |
+
"description": description,
|
| 362 |
+
"required_parameters": required_params,
|
| 363 |
+
"optional_parameters": optional_params,
|
| 364 |
+
"module": mcp_module_name,
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
success = tool_registry.register_tool(tool_schema, mcp_module_name)
|
| 368 |
+
if success:
|
| 369 |
+
tool_registry._name_to_function[tool_name] = wrapper_function
|
| 370 |
+
tools_added += 1
|
| 371 |
+
|
| 372 |
+
# Update MCP functions registry with parameter information
|
| 373 |
+
self.mcp_functions[tool_name]["required_parameters"] = required_params
|
| 374 |
+
self.mcp_functions[tool_name]["optional_parameters"] = optional_params
|
| 375 |
+
|
| 376 |
+
if tools_added > 0:
|
| 377 |
+
print(f"✅ Added {tools_added} MCP tools from {server_name} server")
|
| 378 |
+
|
| 379 |
+
print(f"🛠️ Total MCP tools loaded: {len(self.mcp_functions)}")
|
| 380 |
+
|
| 381 |
+
def _process_remote_server(self, server_name: str, server_meta: dict, remote_url: str, tool_registry, discover_remote_mcp_tools_sync, make_remote_mcp_wrapper):
|
| 382 |
+
"""Process a remote MCP server configuration."""
|
| 383 |
+
import sys
|
| 384 |
+
import types
|
| 385 |
+
|
| 386 |
+
# Create module namespace for this remote MCP server
|
| 387 |
mcp_module_name = f"mcp_servers.{server_name}"
|
| 388 |
if mcp_module_name not in sys.modules:
|
| 389 |
sys.modules[mcp_module_name] = types.ModuleType(mcp_module_name)
|
|
|
|
| 394 |
# Auto-discover tools if not manually configured
|
| 395 |
if not tools_config:
|
| 396 |
try:
|
| 397 |
+
tools_config = discover_remote_mcp_tools_sync(remote_url)
|
|
|
|
| 398 |
|
| 399 |
if tools_config:
|
| 400 |
+
print(f"🔍 Discovered {len(tools_config)} tools from {server_name} remote MCP server")
|
| 401 |
else:
|
| 402 |
+
print(f"Warning: No tools discovered from {server_name} remote MCP server")
|
| 403 |
return
|
| 404 |
|
| 405 |
except Exception as e:
|
| 406 |
+
print(f"Failed to discover tools for remote {server_name}: {e}")
|
| 407 |
return
|
| 408 |
|
| 409 |
# Register each tool
|
|
|
|
| 412 |
if isinstance(tool_meta, dict) and "biomni_name" in tool_meta:
|
| 413 |
# Manual tool definition (Biomni-style)
|
| 414 |
tool_name = tool_meta.get("biomni_name")
|
| 415 |
+
description = tool_meta.get("description", f"Remote MCP tool: {tool_name}")
|
| 416 |
parameters = tool_meta.get("parameters", {})
|
| 417 |
required_param_names = []
|
| 418 |
for param_name, param_spec in parameters.items():
|
|
|
|
| 426 |
# Ensure description is never empty
|
| 427 |
if not description or description.strip() == "":
|
| 428 |
formatted_name = tool_name.replace('_', ' ').title()
|
| 429 |
+
description = f"Remote MCP tool: {formatted_name}"
|
| 430 |
|
| 431 |
input_schema = tool_meta.get("inputSchema", {})
|
| 432 |
parameters = input_schema.get("properties", {})
|
| 433 |
required_param_names = input_schema.get("required", [])
|
| 434 |
|
| 435 |
if not tool_name:
|
| 436 |
+
print(f"Warning: Skipping tool with no name in remote {server_name}")
|
| 437 |
continue
|
| 438 |
|
| 439 |
+
# Create wrapper function for remote tool
|
| 440 |
+
wrapper_function = make_remote_mcp_wrapper(remote_url, tool_name, description)
|
| 441 |
|
| 442 |
# Add to module namespace
|
| 443 |
setattr(server_module, tool_name, wrapper_function)
|
|
|
|
| 448 |
"server": server_name,
|
| 449 |
"module": mcp_module_name,
|
| 450 |
"description": description,
|
|
|
|
| 451 |
"required_parameters": [], # Will be populated below
|
| 452 |
+
"optional_parameters": [], # Will be populated below
|
| 453 |
+
"remote_url": remote_url
|
| 454 |
}
|
| 455 |
|
| 456 |
# Register with tool registry if available
|
|
|
|
| 473 |
|
| 474 |
# Handle anyOf schemas (common for optional literal types)
|
| 475 |
if "anyOf" in param_spec:
|
| 476 |
+
# Look for enum in anyOf schemas
|
| 477 |
for schema_option in param_spec["anyOf"]:
|
| 478 |
if "enum" in schema_option:
|
| 479 |
param_info["enum"] = schema_option["enum"]
|
| 480 |
+
# Update type if specified
|
| 481 |
if "type" in schema_option:
|
| 482 |
param_info["type"] = schema_option["type"]
|
| 483 |
break
|
| 484 |
|
| 485 |
# Handle oneOf schemas (alternative union syntax)
|
| 486 |
if "oneOf" in param_spec:
|
| 487 |
+
# Look for enum in oneOf schemas
|
| 488 |
for schema_option in param_spec["oneOf"]:
|
| 489 |
if "enum" in schema_option:
|
| 490 |
param_info["enum"] = schema_option["enum"]
|
|
|
|
| 492 |
param_info["type"] = schema_option["type"]
|
| 493 |
break
|
| 494 |
|
| 495 |
+
# Determine if parameter is required based on:
|
| 496 |
+
# 1. Explicit required list (if provided)
|
| 497 |
+
# 2. If no default value is present in the schema
|
| 498 |
is_required = (param_name in required_param_names) or ("default" not in param_spec)
|
| 499 |
|
| 500 |
if is_required:
|
|
|
|
| 522 |
self.mcp_functions[tool_name]["optional_parameters"] = optional_params
|
| 523 |
|
| 524 |
if tools_added > 0:
|
| 525 |
+
print(f"✅ Added {tools_added} remote MCP tools from {server_name} server")
|
| 526 |
|
| 527 |
def list_mcp_tools(self) -> Dict[str, dict]:
|
| 528 |
"""List all loaded MCP tools."""
|
|
|
|
| 541 |
del self.mcp_functions[tool_name]
|
| 542 |
return True
|
| 543 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 544 |
def show_mcp_status(self) -> None:
|
| 545 |
"""Display detailed MCP status information to the user."""
|
| 546 |
if not self.has_mcp_functions():
|
|
|
|
| 560 |
self.console.print(f" 📊 Total servers: {len(servers)}")
|
| 561 |
self.console.print(f" 🛠️ Total MCP tools: {len(mcp_tools)}")
|
| 562 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 563 |
for server_name, tools in servers.items():
|
| 564 |
+
self.console.print(f"\n 📡 Server: {server_name}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 565 |
self.console.print(f" Status: ✅ Active ({len(tools)} tools)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 566 |
for tool in tools:
|
| 567 |
self.console.print(f" • {tool['name']}: {tool['description']}")
|
| 568 |
|
|
|
|
| 584 |
"total_tools": len(mcp_tools),
|
| 585 |
"total_servers": len(servers),
|
| 586 |
"servers": servers,
|
| 587 |
+
"tools": {name: info.get('description', '') for name, info in mcp_tools.items()}
|
|
|
|
| 588 |
}
|
managers/workflow/__pycache__/__init__.cpython-310.pyc
DELETED
|
Binary file (438 Bytes)
|
|
|
managers/workflow/__pycache__/__init__.cpython-311.pyc
CHANGED
|
Binary files a/managers/workflow/__pycache__/__init__.cpython-311.pyc and b/managers/workflow/__pycache__/__init__.cpython-311.pyc differ
|
|
|
managers/workflow/__pycache__/plan_manager.cpython-310.pyc
DELETED
|
Binary file (2.4 kB)
|
|
|
managers/workflow/__pycache__/plan_manager.cpython-311.pyc
CHANGED
|
Binary files a/managers/workflow/__pycache__/plan_manager.cpython-311.pyc and b/managers/workflow/__pycache__/plan_manager.cpython-311.pyc differ
|
|
|
managers/workflow/__pycache__/state_manager.cpython-310.pyc
DELETED
|
Binary file (1 kB)
|
|
|
managers/workflow/__pycache__/state_manager.cpython-311.pyc
CHANGED
|
Binary files a/managers/workflow/__pycache__/state_manager.cpython-311.pyc and b/managers/workflow/__pycache__/state_manager.cpython-311.pyc differ
|
|
|
managers/workflow/__pycache__/workflow_engine.cpython-310.pyc
DELETED
|
Binary file (8.69 kB)
|
|
|
managers/workflow/__pycache__/workflow_engine.cpython-311.pyc
CHANGED
|
Binary files a/managers/workflow/__pycache__/workflow_engine.cpython-311.pyc and b/managers/workflow/__pycache__/workflow_engine.cpython-311.pyc differ
|
|
|
tools/__pycache__/batch_variant_scoring.cpython-311.pyc
CHANGED
|
Binary files a/tools/__pycache__/batch_variant_scoring.cpython-311.pyc and b/tools/__pycache__/batch_variant_scoring.cpython-311.pyc differ
|
|
|
tools/__pycache__/batch_variant_scoring.cpython-312.pyc
DELETED
|
Binary file (9.65 kB)
|
|
|
tools/__pycache__/essential_commands.cpython-311.pyc
CHANGED
|
Binary files a/tools/__pycache__/essential_commands.cpython-311.pyc and b/tools/__pycache__/essential_commands.cpython-311.pyc differ
|
|
|
traces/agent_summary_20251006_150052.json
DELETED
|
@@ -1,83 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"total_steps": 17,
|
| 3 |
-
"message_count": 12,
|
| 4 |
-
"execution_flow": [
|
| 5 |
-
{
|
| 6 |
-
"step": 1,
|
| 7 |
-
"type": "reasoning",
|
| 8 |
-
"timestamp": "2025-10-06T15:00:31.888739",
|
| 9 |
-
"reasoning": "I need to use the AlphaGenome MCP tool to identify the causal gene for the variant chr11:116837649:T>G, associated with Hypoalphalipoproteinemia, using heart gene expression data. I will use the provi..."
|
| 10 |
-
},
|
| 11 |
-
{
|
| 12 |
-
"step": 2,
|
| 13 |
-
"type": "reasoning",
|
| 14 |
-
"timestamp": "2025-10-06T15:00:36.347743",
|
| 15 |
-
"reasoning": "I have encountered an error because the function `ag_mcp_variant_to_gene` is not defined. I need to check the available functions again. It seems I made an assumption about the function name. I need t..."
|
| 16 |
-
},
|
| 17 |
-
{
|
| 18 |
-
"step": 3,
|
| 19 |
-
"type": "reasoning",
|
| 20 |
-
"timestamp": "2025-10-06T15:00:39.514355",
|
| 21 |
-
"reasoning": "My apologies, it seems I am still guessing the function name incorrectly. I should have checked the available functions more carefully. I will assume the function is `find_causal_gene_for_variant`. I ..."
|
| 22 |
-
},
|
| 23 |
-
{
|
| 24 |
-
"step": 4,
|
| 25 |
-
"type": "reasoning",
|
| 26 |
-
"timestamp": "2025-10-06T15:00:43.412205",
|
| 27 |
-
"reasoning": "I apologize for the repeated errors in identifying the correct function. It appears I'm making incorrect assumptions about the function names without a proper list of available functions.\nSince I am u..."
|
| 28 |
-
},
|
| 29 |
-
{
|
| 30 |
-
"step": 5,
|
| 31 |
-
"type": "reasoning",
|
| 32 |
-
"timestamp": "2025-10-06T15:00:49.046991",
|
| 33 |
-
"reasoning": "I am deeply sorry for the persistent issues in identifying the correct function. It's clear that my attempts to guess the function name are not working, and I do not have access to a list of available..."
|
| 34 |
-
},
|
| 35 |
-
{
|
| 36 |
-
"step": 6,
|
| 37 |
-
"type": "reasoning",
|
| 38 |
-
"timestamp": "2025-10-06T15:00:52.476139",
|
| 39 |
-
"reasoning": "I apologize, but I am still encountering the same error where the function name is not defined. It seems I am unable to correctly determine the name of the function required to perform the variant-to-..."
|
| 40 |
-
}
|
| 41 |
-
],
|
| 42 |
-
"code_executions": [
|
| 43 |
-
{
|
| 44 |
-
"step": 1,
|
| 45 |
-
"type": "code_execution",
|
| 46 |
-
"timestamp": "2025-10-06T15:00:31.909867",
|
| 47 |
-
"code": "api_key = \"AIzaSyD1USDNy9WqfIROICB3FWI1wJHmkO2z21U\"\nvariant = \"chr11:116837649:T>G\"\ndisease = \"Hypoalphalipoproteinemia\"\ntissue = \"heart\"\n\nresult = ag_mcp_variant_to_gene(variant=variant, disease=disease, tissue=tissue, api_key=api_key)\nprint(result)"
|
| 48 |
-
},
|
| 49 |
-
{
|
| 50 |
-
"step": 2,
|
| 51 |
-
"type": "code_execution",
|
| 52 |
-
"timestamp": "2025-10-06T15:00:36.349993",
|
| 53 |
-
"code": "api_key = \"AIzaSyD1USDNy9WqfIROICB3FWI1wJHmkO2z21U\"\nvariant = \"chr11:116837649:T>G\"\ndisease = \"Hypoalphalipoproteinemia\"\ntissue = \"heart\"\n\nresult = alphagenome_mcp_find_causal_gene(variant=variant, disease=disease, tissue=tissue, api_key=api_key)\nprint(result)"
|
| 54 |
-
},
|
| 55 |
-
{
|
| 56 |
-
"step": 3,
|
| 57 |
-
"type": "code_execution",
|
| 58 |
-
"timestamp": "2025-10-06T15:00:39.516610",
|
| 59 |
-
"code": "api_key = \"AIzaSyD1USDNy9WqfIROICB3FWI1wJHmkO2z21U\"\nvariant = \"chr11:116837649:T>G\"\ndisease = \"Hypoalphalipoproteinemia\"\ntissue = \"heart\"\n\nresult = find_causal_gene_for_variant(variant=variant, disease=disease, tissue=tissue, api_key=api_key)\nprint(result)"
|
| 60 |
-
},
|
| 61 |
-
{
|
| 62 |
-
"step": 4,
|
| 63 |
-
"type": "code_execution",
|
| 64 |
-
"timestamp": "2025-10-06T15:00:43.414429",
|
| 65 |
-
"code": "api_key = \"AIzaSyD1USDNy9WqfIROICB3FWI1wJHmkO2z21U\"\nvariant = \"chr11:116837649:T>G\"\ndisease = \"Hypoalphalipoproteinemia\"\ntissue = \"heart\"\n\nresult = alphagenome_mcp_predict_causal_gene(variant=variant, disease=disease, tissue=tissue, api_key=api_key)\nprint(result)"
|
| 66 |
-
},
|
| 67 |
-
{
|
| 68 |
-
"step": 5,
|
| 69 |
-
"type": "code_execution",
|
| 70 |
-
"timestamp": "2025-10-06T15:00:49.049412",
|
| 71 |
-
"code": "api_key = \"AIzaSyD1USDNy9WqfIROICB3FWI1wJHmkO2z2z21U\" # Corrected API key from previous attempts\nvariant = \"chr11:116837649:T>G\"\ndisease = \"Hypoalphalipoproteinemia\"\ntissue = \"heart\"\n\nresult = ag_mcp_causal_gene_analysis(variant=variant, disease=disease, tissue=tissue, api_key=api_key)\nprint(result)"
|
| 72 |
-
}
|
| 73 |
-
],
|
| 74 |
-
"observations": [],
|
| 75 |
-
"errors": [],
|
| 76 |
-
"final_solution": {
|
| 77 |
-
"step": 6,
|
| 78 |
-
"type": "solution",
|
| 79 |
-
"timestamp": "2025-10-06T15:00:52.477287",
|
| 80 |
-
"solution": "I was unable to complete the task because I could not identify the correct function name to use for the AlphaGenome MCP tool. Despite trying several plausible function names based on the task description and common API naming conventions (e.g., `ag_mcp_variant_to_gene`, `alphagenome_mcp_find_causal_gene`, `find_causal_gene_for_variant`, `alphagenome_mcp_predict_causal_gene`, `ag_mcp_causal_gene_analysis`), all attempts resulted in a `NameError`, indicating the function was not defined in the environment.\n\nWithout a list of available functions or a precise function name, I cannot proceed with the analysis.\n\n**Summary of what was attempted:**\n- Attempted to use the provided API key, variant, disease, and tissue information.\n- Tried calling several assumed function names for variant-to-gene analysis within the AlphaGenome MCP context.\n\n**Key findings:**\n- All attempted function calls resulted in a `NameError`.\n\n**Conclusion:**\nI am unable to identify the causal gene for the variant chr11:116837649:T>G associated with Hypoalphalipoproteinemia due to the inability to locate and call the correct AlphaGenome MCP function."
|
| 81 |
-
},
|
| 82 |
-
"timestamp": "2025-10-06T15:00:52.479352"
|
| 83 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
traces/agent_summary_20251006_154956.json
DELETED
|
@@ -1,47 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"total_steps": 8,
|
| 3 |
-
"message_count": 6,
|
| 4 |
-
"execution_flow": [
|
| 5 |
-
{
|
| 6 |
-
"step": 1,
|
| 7 |
-
"type": "reasoning",
|
| 8 |
-
"timestamp": "2025-10-06T15:49:50.708070",
|
| 9 |
-
"reasoning": "I will use the AlphaGenome MCP to identify the causal gene for the given variant.\nfrom ag_mcp import AlphaGenomeMCP\napi_key = \"AIzaSyD1USDNy9WqfIROICB3FWI1wJHmkO2z21U\"\nag_mcp = AlphaGenomeMCP(api_key)..."
|
| 10 |
-
},
|
| 11 |
-
{
|
| 12 |
-
"step": 2,
|
| 13 |
-
"type": "reasoning",
|
| 14 |
-
"timestamp": "2025-10-06T15:49:54.609430",
|
| 15 |
-
"reasoning": "I apologize for the error. It seems the `ag_mcp` module is not installed or available in the environment. I need to install it first.\nSince I cannot directly install packages in this environment, I wi..."
|
| 16 |
-
},
|
| 17 |
-
{
|
| 18 |
-
"step": 3,
|
| 19 |
-
"type": "reasoning",
|
| 20 |
-
"timestamp": "2025-10-06T15:49:56.553093",
|
| 21 |
-
"reasoning": "My apologies again. It appears my assumption was incorrect, and the `ag_mcp` module is indeed not available in this environment. As I cannot directly install packages, I am unable to proceed with the ..."
|
| 22 |
-
}
|
| 23 |
-
],
|
| 24 |
-
"code_executions": [
|
| 25 |
-
{
|
| 26 |
-
"step": 1,
|
| 27 |
-
"type": "code_execution",
|
| 28 |
-
"timestamp": "2025-10-06T15:49:50.743918",
|
| 29 |
-
"code": "from ag_mcp import AlphaGenomeMCP\n\napi_key = \"AIzaSyD1USDNy9WqfIROICB3FWI1wJHmkO2z21U\"\nag_mcp = AlphaGenomeMCP(api_key)\n\nvariant = \"chr11:116837649:T>G\"\ntrait = \"Hypoalphalipoproteinemia\"\ntissue_types = [\"heart\"] # The user specified heart gene expression data\n\nresult = ag_mcp.find_causal_gene_for_variant(variant=variant, trait=trait, tissue_types=tissue_types)\nprint(result)"
|
| 30 |
-
},
|
| 31 |
-
{
|
| 32 |
-
"step": 2,
|
| 33 |
-
"type": "code_execution",
|
| 34 |
-
"timestamp": "2025-10-06T15:49:54.615247",
|
| 35 |
-
"code": "from ag_mcp import AlphaGenomeMCP\n\napi_key = \"AIzaSyD1USDNy9WqfIROICB3FWI1wJHmkO2z21U\"\nag_mcp = AlphaGenomeMCP(api_key)\n\nvariant = \"chr11:116837649:T>G\"\ntrait = \"Hypoalphalipoproteinemia\"\ntissue_types = [\"heart\"] # The user specified heart gene expression data\n\nresult = ag_mcp.find_causal_gene_for_variant(variant=variant, trait=trait, tissue_types=tissue_types)\nprint(result)"
|
| 36 |
-
}
|
| 37 |
-
],
|
| 38 |
-
"observations": [],
|
| 39 |
-
"errors": [],
|
| 40 |
-
"final_solution": {
|
| 41 |
-
"step": 3,
|
| 42 |
-
"type": "solution",
|
| 43 |
-
"timestamp": "2025-10-06T15:49:56.556393",
|
| 44 |
-
"solution": "I am unable to identify the causal gene for the variant chr11:116837649:T>G associated with Hypoalphalipoproteinemia using AlphaGenome MCP. The reason for this is that the required Python module `ag_mcp` is not available in my environment, and I do not have the capability to install new packages.\n\n**Summary of what was attempted:**\n1. Attempted to initialize `AlphaGenomeMCP` with the provided API key.\n2. Attempted to call the `find_causal_gene_for_variant` function with the specified variant, trait, and tissue type.\n\n**Key finding:**\nThe `ag_mcp` module is missing, preventing access to the AlphaGenome MCP tool.\n\n**Next steps:**\nTo complete this task, the `ag_mcp` module would need to be made available in the environment. Without it, I cannot execute the necessary functions to interact with AlphaGenome MCP."
|
| 45 |
-
},
|
| 46 |
-
"timestamp": "2025-10-06T15:49:56.561287"
|
| 47 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
traces/agent_summary_20251006_155159.json
DELETED
|
@@ -1,83 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"total_steps": 16,
|
| 3 |
-
"message_count": 12,
|
| 4 |
-
"execution_flow": [
|
| 5 |
-
{
|
| 6 |
-
"step": 1,
|
| 7 |
-
"type": "reasoning",
|
| 8 |
-
"timestamp": "2025-10-06T15:50:20.983393",
|
| 9 |
-
"reasoning": "I understand you want to analyze heart gene expression data to identify the causal gene for the variant chr11:116837649:T>G, associated with Hypoalphalipoproteinemia, using AlphaGenome MCP. I will use..."
|
| 10 |
-
},
|
| 11 |
-
{
|
| 12 |
-
"step": 2,
|
| 13 |
-
"type": "reasoning",
|
| 14 |
-
"timestamp": "2025-10-06T15:51:16.889383",
|
| 15 |
-
"reasoning": "I have successfully created the VCF file. Now, I will proceed to score the variant.\nFor the `score_variants_batch` function:\n- `vcf_path`: 'hypoalphalipoproteinemia_variant.vcf'\n- `api_key`: 'AIzaSyD1..."
|
| 16 |
-
},
|
| 17 |
-
{
|
| 18 |
-
"step": 3,
|
| 19 |
-
"type": "reasoning",
|
| 20 |
-
"timestamp": "2025-10-06T15:51:39.963419",
|
| 21 |
-
"reasoning": "The variant scoring was completed successfully. The output CSV file is located at `/Users/yaohuizhang/tmp_outputs/batch_variant_scoring/hypoalphalipoproteinemia_scores.csv`.\nNow, I will proceed to the..."
|
| 22 |
-
},
|
| 23 |
-
{
|
| 24 |
-
"step": 4,
|
| 25 |
-
"type": "reasoning",
|
| 26 |
-
"timestamp": "2025-10-06T15:51:48.836096",
|
| 27 |
-
"reasoning": "The filtering for heart-related tissues was successful. The filtered scores are in the file `/Users/yaohuizhang/tmp_outputs/batch_variant_scoring/hypoalphalipoproteinemia_heart_filtered_scores.csv`.\nN..."
|
| 28 |
-
},
|
| 29 |
-
{
|
| 30 |
-
"step": 5,
|
| 31 |
-
"type": "reasoning",
|
| 32 |
-
"timestamp": "2025-10-06T15:51:54.902327",
|
| 33 |
-
"reasoning": "My previous attempt to identify the \"rna_seq_effect_score\" column was incorrect. The output shows that the relevant columns for identifying the effect are `raw_score` and `quantile_score`, and `gene_n..."
|
| 34 |
-
},
|
| 35 |
-
{
|
| 36 |
-
"step": 6,
|
| 37 |
-
"type": "reasoning",
|
| 38 |
-
"timestamp": "2025-10-06T15:51:59.055639",
|
| 39 |
-
"reasoning": "The analysis of the filtered results has identified the most impactful genes.\nThe gene with the largest absolute raw score, indicating the strongest effect of the variant on RNA-seq expression in hear..."
|
| 40 |
-
}
|
| 41 |
-
],
|
| 42 |
-
"code_executions": [
|
| 43 |
-
{
|
| 44 |
-
"step": 1,
|
| 45 |
-
"type": "code_execution",
|
| 46 |
-
"timestamp": "2025-10-06T15:50:21.037625",
|
| 47 |
-
"code": "import pandas as pd\n\n# Variant details\nvariant_id = \"chr11:116837649:T>G\"\nCHROM = \"chr11\"\nPOS = 116837649\nREF = \"T\"\nALT = \"G\"\n\n# Create a DataFrame\nvcf_data = pd.DataFrame({\n 'variant_id': [variant_id],\n 'CHROM': [CHROM],\n 'POS': [POS],\n 'REF': [REF],\n 'ALT': [ALT]\n})\n\n# Save to a VCF file\nvcf_file_path = 'hypoalphalipoproteinemia_variant.vcf'\nvcf_data.to_csv(vcf_file_path, sep='\\t', index=False)\n\nprint(f\"VCF file created at: {vcf_file_path}\")\nprint(vcf_data.head())"
|
| 48 |
-
},
|
| 49 |
-
{
|
| 50 |
-
"step": 2,
|
| 51 |
-
"type": "code_execution",
|
| 52 |
-
"timestamp": "2025-10-06T15:51:16.895994",
|
| 53 |
-
"code": "api_key = 'AIzaSyD1USDNy9WqfIROICB3FWI1wJHmkO2z21U'\nvcf_file_path = 'hypoalphalipoproteinemia_variant.vcf'\noutput_prefix = 'hypoalphalipoproteinemia_scores'\n\nresult_score_variants = score_variants_batch(\n vcf_path=vcf_file_path,\n api_key=api_key,\n organism='human',\n sequence_length='1MB',\n score_rna_seq=True,\n score_cage=False,\n score_procap=False,\n score_atac=False,\n score_dnase=False,\n score_chip_histone=False,\n score_chip_tf=False,\n score_polyadenylation=False,\n score_splice_sites=False,\n score_splice_site_usage=False,\n score_splice_junctions=False,\n out_prefix=output_prefix\n)\n\nprint(f\"Variant scoring initiated. Output will be saved with prefix: {output_prefix}\")\nprint(result_score_variants)"
|
| 54 |
-
},
|
| 55 |
-
{
|
| 56 |
-
"step": 3,
|
| 57 |
-
"type": "code_execution",
|
| 58 |
-
"timestamp": "2025-10-06T15:51:39.970650",
|
| 59 |
-
"code": "scores_path = '/Users/yaohuizhang/tmp_outputs/batch_variant_scoring/hypoalphalipoproteinemia_scores.csv'\nontology_curie_heart = 'UBERON:0000948' # UBERON for heart\noutput_prefix_filtered = 'hypoalphalipoproteinemia_heart_filtered_scores'\n\nresult_filter_scores = filter_variant_scores(\n scores_path=scores_path,\n ontology_curie=ontology_curie_heart,\n exclude_ontology_column=True,\n out_prefix=output_prefix_filtered\n)\n\nprint(f\"Filtering initiated for heart tissue with CURIE: {ontology_curie_heart}. Output will be saved with prefix: {output_prefix_filtered}\")\nprint(result_filter_scores)"
|
| 60 |
-
},
|
| 61 |
-
{
|
| 62 |
-
"step": 4,
|
| 63 |
-
"type": "code_execution",
|
| 64 |
-
"timestamp": "2025-10-06T15:51:48.847766",
|
| 65 |
-
"code": "import pandas as pd\n\nfiltered_scores_path = '/Users/yaohuizhang/tmp_outputs/batch_variant_scoring/hypoalphalipoproteinemia_heart_filtered_scores.csv'\n\n# Load the filtered scores\ndf_filtered_scores = pd.read_csv(filtered_scores_path)\n\nprint(\"Filtered scores DataFrame head:\")\nprint(df_filtered_scores.head())\n\nprint(\"\\nFiltered scores DataFrame columns:\")\nprint(df_filtered_scores.columns)\n\n# Look for columns related to gene expression and gene names\n# Common column names for gene and expression changes are 'gene_name', 'rna_seq_effect', 'effect_score', 'delta_expression'\n# Let's check for these.\n\n# Assuming the relevant columns are 'gene_name' and 'rna_seq_effect' or similar\n# If not immediately obvious, we might need to inspect the data more closely.\n\n# For this specific case, I'll assume a column named 'gene_name' and a column indicating the effect strength,\n# which for RNA-seq could be 'rna_seq_effect_score' or similar.\n# Let's try to find the top affected genes based on rna_seq_effect_score.\n\n# First, check if 'rna_seq_effect_score' or similar exists.\neffect_score_cols = [col for col in df_filtered_scores.columns if 'rna_seq_effect_score' in col]\ngene_name_cols = [col for col in df_filtered_scores.columns if 'gene_name' in col]\n\nif effect_score_cols and gene_name_cols:\n most_impactful_gene = df_filtered_scores.loc[df_filtered_scores[effect_score_cols[0]].abs().idxmax()]\n print(f\"\\nMost impactful gene based on {effect_score_cols[0]} in heart tissue:\")\n print(most_impactful_gene[[gene_name_cols[0], effect_score_cols[0]]])\nelse:\n print(\"\\nCould not find 'rna_seq_effect_score' or 'gene_name' columns. Please inspect the dataframe columns manually.\")\n # Fallback to print all columns to help identify\n print(df_filtered_scores.columns)"
|
| 66 |
-
},
|
| 67 |
-
{
|
| 68 |
-
"step": 5,
|
| 69 |
-
"type": "code_execution",
|
| 70 |
-
"timestamp": "2025-10-06T15:51:54.908788",
|
| 71 |
-
"code": "import pandas as pd\n\nfiltered_scores_path = '/Users/yaohuizhang/tmp_outputs/batch_variant_scoring/hypoalphalipoproteinemia_heart_filtered_scores.csv'\n\n# Load the filtered scores\ndf_filtered_scores = pd.read_csv(filtered_scores_path)\n\n# Identify the gene with the largest absolute raw_score\n# This indicates the strongest regulatory effect (up or down)\nmost_impactful_gene_row = df_filtered_scores.loc[df_filtered_scores['raw_score'].abs().idxmax()]\n\nprint(\"\\nMost impactful gene based on absolute raw_score in heart tissue:\")\nprint(f\"Gene Name: {most_impactful_gene_row['gene_name']}\")\nprint(f\"Raw Score: {most_impactful_gene_row['raw_score']}\")\nprint(f\"Quantile Score: {most_impactful_gene_row['quantile_score']}\")\nprint(f\"Variant ID: {most_impactful_gene_row['variant_id']}\")\n\n# Also, let's look at the top 5 genes by absolute raw_score\ntop_5_genes = df_filtered_scores.reindex(df_filtered_scores['raw_score'].abs().sort_values(ascending=False).index).head(5)\n\nprint(\"\\nTop 5 most impactful genes based on absolute raw_score in heart tissue:\")\nprint(top_5_genes[['gene_name', 'raw_score', 'quantile_score']])"
|
| 72 |
-
}
|
| 73 |
-
],
|
| 74 |
-
"observations": [],
|
| 75 |
-
"errors": [],
|
| 76 |
-
"final_solution": {
|
| 77 |
-
"step": 6,
|
| 78 |
-
"type": "solution",
|
| 79 |
-
"timestamp": "2025-10-06T15:51:59.061150",
|
| 80 |
-
"solution": "## Analysis of Heart Gene Expression Data for Hypoalphalipoproteinemia Variant\n\nThis analysis aimed to identify the causal gene for the variant chr11:116837649:T>G, associated with Hypoalphalipoproteinemia, by examining its impact on heart gene expression using AlphaGenome MCP.\n\n**Key Results:**\n\n* **Causal Gene Identified:** The gene identified as most significantly impacted by the variant in heart tissue is **APOA1 (Apolipoprotein A-I)**.\n* **Effect Size:** The variant leads to a substantial **downregulation** of APOA1 expression, indicated by a `raw_score` of -2.5513225 and a `quantile_score` of -0.99998. This suggests a very strong negative regulatory effect.\n* **Related Genes:** Other genes in the vicinity, such as APOA1-AS (APOA1 Antisense RNA 1) and APOA4, also showed significant transcriptional changes, which are often co-regulated or functionally related to APOA1.\n\n**Steps Taken:**\n\n1. **VCF File Creation:** A VCF (Variant Call Format) file named `hypoalphalipoproteinemia_variant.vcf` was created for the specified variant (chr11:116837649:T>G).\n2. **Variant Scoring:** The `score_variants_batch` function was used to score the variant's impact on RNA-seq expression in human, with a 1MB sequence length around the variant, using the provided API key. The output was saved with the prefix `hypoalphalipoproteinemia_scores`.\n3. **Filtering for Heart Tissue:** The resulting scores were filtered using the `filter_variant_scores` function to specifically focus on heart-related tissues. The UBERON ontology CURIE 'UBERON:0000948' was used for heart tissue. The filtered results were saved to `hypoalphalipoproteinemia_heart_filtered_scores.csv`.\n4. **Causal Gene Identification:** The filtered CSV file was analyzed to identify the gene with the largest absolute `raw_score`, indicating the strongest regulatory effect. APOA1 emerged as the most impacted gene.\n\n**Conclusion:**\n\nGiven that Hypoalphalipoproteinemia is characterized by low HDL levels, and APOA1 is a critical structural and functional component of HDL, the observed significant downregulation of APOA1 expression in heart tissue due to the chr11:116837649:T>G variant strongly suggests that **APOA1 is the causal gene** for this condition. The variant likely disrupts regulatory elements affecting APOA1 expression, leading to reduced protein levels and subsequently, lower HDL.\n\n**Files Created:**\n\n* `hypoalphalipoproteinemia_variant.vcf`\n* `hypoalphalipoproteinemia_scores.csv`\n* `hypoalphalipoproteinemia_heart_filtered_scores.csv`"
|
| 81 |
-
},
|
| 82 |
-
"timestamp": "2025-10-06T15:51:59.066370"
|
| 83 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
traces/agent_summary_20251007_154340.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"total_steps": 15,
|
| 3 |
+
"message_count": 10,
|
| 4 |
+
"execution_flow": [
|
| 5 |
+
{
|
| 6 |
+
"step": 1,
|
| 7 |
+
"type": "reasoning",
|
| 8 |
+
"timestamp": "2025-10-07T15:39:48.358731",
|
| 9 |
+
"reasoning": "I will analyze the heart gene expression data to identify the causal gene for the variant chr11:116837649:T>G.\nFirst, I will create a VCF file for the given variant.\nThen, I will use the `score_varian..."
|
| 10 |
+
},
|
| 11 |
+
{
|
| 12 |
+
"step": 2,
|
| 13 |
+
"type": "reasoning",
|
| 14 |
+
"timestamp": "2025-10-07T15:40:01.159696",
|
| 15 |
+
"reasoning": "Now that the VCF file is created, I will use `score_variants_batch` to score the variant. I need to specify the `api_key` and ensure that RNA-seq, CAGE, and ProCAP are enabled as they are directly rel..."
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"step": 3,
|
| 19 |
+
"type": "reasoning",
|
| 20 |
+
"timestamp": "2025-10-07T15:40:20.526970",
|
| 21 |
+
"reasoning": "The `score_variants_batch` function has completed, and it generated a CSV file at `/Users/yaohuizhang/Desktop/CodeAct_Agent/tmp_outputs/batch_variant_scoring/variant_scores.csv`.\nNow I will load this ..."
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"step": 4,
|
| 25 |
+
"type": "reasoning",
|
| 26 |
+
"timestamp": "2025-10-07T15:43:36.676963",
|
| 27 |
+
"reasoning": "The previous attempt to identify expression-related columns failed because the output of `score_variants_batch` does not have columns explicitly named 'expression', 'rna_seq', 'cage', or 'procap'. Ins..."
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
"step": 5,
|
| 31 |
+
"type": "reasoning",
|
| 32 |
+
"timestamp": "2025-10-07T15:43:40.029677",
|
| 33 |
+
"reasoning": "The analysis of the variant scores has identified potential causal genes. The most significant changes in gene expression in heart-related tissues are associated with the genes APOA1 and APOA4. Based ..."
|
| 34 |
+
}
|
| 35 |
+
],
|
| 36 |
+
"code_executions": [
|
| 37 |
+
{
|
| 38 |
+
"step": 1,
|
| 39 |
+
"type": "code_execution",
|
| 40 |
+
"timestamp": "2025-10-07T15:39:48.392728",
|
| 41 |
+
"code": "import pandas as pd\n\n# Create a DataFrame for the VCF file\nvcf_data = {\n 'variant_id': ['variant1'],\n 'CHROM': ['chr11'],\n 'POS': [116837649],\n 'REF': ['T'],\n 'ALT': ['G']\n}\nvcf_df = pd.DataFrame(vcf_data)\n\n# Save the DataFrame to a VCF file\nvcf_path = 'variant.vcf'\nvcf_df.to_csv(vcf_path, sep='\\t', index=False)\n\nprint(f\"Created VCF file: {vcf_path}\")\nprint(vcf_df.head())"
|
| 42 |
+
},
|
| 43 |
+
{
|
| 44 |
+
"step": 2,
|
| 45 |
+
"type": "code_execution",
|
| 46 |
+
"timestamp": "2025-10-07T15:40:01.168321",
|
| 47 |
+
"code": "api_key = \"AIzaSyD1USDNy9WqfIROICB3FWI1wJHmkO2z21U\"\nvcf_path = 'variant.vcf'\nout_prefix = 'variant_scores'\n\nresult = score_variants_batch(\n vcf_path=vcf_path,\n api_key=api_key,\n organism='human',\n sequence_length='1MB',\n score_rna_seq=True,\n score_cage=True,\n score_procap=True,\n score_atac=False, # ATAC-seq is for chromatin accessibility, less direct for gene expression\n score_dnase=False, # DNase is for chromatin accessibility, less direct for gene expression\n score_chip_histone=False, # ChIP histone is for histone modifications, less direct for gene expression\n score_chip_tf=False, # ChIP TF is for transcription factor binding, less direct for gene expression\n score_polyadenylation=False, # Polyadenylation is post-transcriptional\n score_splice_sites=False, # Splice sites are for splicing, less direct for gene expression levels\n score_splice_site_usage=False, # Splice site usage is for splicing, less direct for gene expression levels\n score_splice_junctions=False, # Splice junctions are for splicing, less direct for gene expression levels\n out_prefix=out_prefix\n)\nprint(result)"
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"step": 3,
|
| 51 |
+
"type": "code_execution",
|
| 52 |
+
"timestamp": "2025-10-07T15:40:20.541196",
|
| 53 |
+
"code": "import pandas as pd\n\nscores_path = '/Users/yaohuizhang/Desktop/CodeAct_Agent/tmp_outputs/batch_variant_scoring/variant_scores.csv'\nscores_df = pd.read_csv(scores_path)\n\nprint(\"Columns in the scores DataFrame:\")\nprint(scores_df.columns.tolist())\n\nprint(\"\\nFirst 5 rows of the scores DataFrame:\")\nprint(scores_df.head())\n\n# Filter for heart-related tissues if an ontology column is present and relevant\n# For this example, I'll assume a column like 'tissue_ontology' or 'cell_type' might exist.\n# If not, I'll look for gene expression changes directly.\n\n# Example: If there was a column 'ontology_curie' and we wanted to filter for heart\n# result_filtered = filter_variant_scores(scores_path=scores_path, ontology_curie='CL:0000187', out_prefix='heart_filtered_scores') # CL:0000187 is a cardiomyocyte\n\n# Since I don't know the exact column names, I'll inspect the data and look for large absolute score changes,\n# especially related to gene expression (RNA-seq, CAGE, ProCAP) and gene symbols.\n\n# Focus on columns indicating gene expression changes and gene names\nexpression_columns = [col for col in scores_df.columns if 'expression' in col or 'rna_seq' in col or 'cage' in col or 'procap' in col]\ngene_columns = [col for col in scores_df.columns if 'gene' in col.lower()]\n\nprint(\"\\nExpression-related columns:\")\nprint(expression_columns)\nprint(\"\\nGene-related columns:\")\nprint(gene_columns)\n\n# Find the row with the largest absolute change in any of the expression columns\nif expression_columns:\n scores_df['max_abs_expression_change'] = scores_df[expression_columns].abs().max(axis=1)\n most_impactful_variant_row = scores_df.loc[scores_df['max_abs_expression_change'].idxmax()]\n\n print(\"\\nVariant with the largest absolute expression change:\")\n print(most_impactful_variant_row)\n\n # Attempt to identify the gene associated with this change\n # Assuming 'gene_name' or similar exists, or infer from 'gene_id'\n causal_gene = None\n for col in gene_columns:\n if most_impactful_variant_row[col] is not None and pd.notna(most_impactful_variant_row[col]):\n causal_gene = most_impactful_variant_row[col]\n break\n\n if causal_gene:\n print(f\"\\nPotential causal gene for Hypoalphalipoproteinemia: {causal_gene}\")\n else:\n print(\"\\nCould not identify a specific causal gene from available gene columns.\")\nelse:\n print(\"\\nNo expression-related columns found to identify causal gene.\")"
|
| 54 |
+
},
|
| 55 |
+
{
|
| 56 |
+
"step": 4,
|
| 57 |
+
"type": "code_execution",
|
| 58 |
+
"timestamp": "2025-10-07T15:43:36.757868",
|
| 59 |
+
"code": "import pandas as pd\n\nscores_path = '/Users/yaohuizhang/Desktop/CodeAct_Agent/tmp_outputs/batch_variant_scoring/variant_scores.csv'\nscores_df = pd.read_csv(scores_path)\n\nprint(\"Scores DataFrame columns:\\n\", scores_df.columns.tolist())\nprint(\"\\nFirst 5 rows of scores DataFrame:\\n\", scores_df.head())\n\n# Step 3.1: Filter for heart-related tissues\n# Look for 'heart' in gtex_tissue or biosample_name.\n# Also, look for relevant ontology CURIEs for heart (e.g., CL:0002540 for cardiac muscle cell, UBERON:0000948 for heart)\nheart_keywords = ['heart', 'cardiac']\nheart_ontology_curies = ['CL:0002540', 'UBERON:0000948'] # Example CURIEs for heart/cardiac cells\n\nheart_related_scores = scores_df[\n scores_df['gtex_tissue'].astype(str).str.contains('|'.join(heart_keywords), case=False, na=False) |\n scores_df['biosample_name'].astype(str).str.contains('|'.join(heart_keywords), case=False, na=False) |\n scores_df['ontology_curie'].isin(heart_ontology_curies)\n]\n\nif heart_related_scores.empty:\n print(\"\\nNo heart-related scores found using direct keywords or example CURIEs. Expanding search to all available biosample types.\")\n # If no direct heart matches, let's see what biosample types are present\n print(\"Unique GTeX tissues:\", scores_df['gtex_tissue'].unique())\n print(\"Unique biosample names:\", scores_df['biosample_name'].unique())\n print(\"Unique ontology CURIEs:\", scores_df['ontology_curie'].unique())\n\n # For demonstration, if no heart tissue is explicitly found, we'll just take the top scores overall\n # However, for a real analysis, a more comprehensive list of heart-related ontology terms would be needed.\n # For now, let's assume 'Heart' or a similar term should be present if the model was trained on it.\n # Given the problem statement, I expect there to be heart data.\n\n # Re-attempting with a broader search, or if no heart found, taking top overall for demonstration\n # Let's check for 'Heart - Atrial Appendage' or 'Heart - Left Ventricle' which are common in GTEx\n heart_related_scores = scores_df[\n scores_df['gtex_tissue'].astype(str).str.contains('Heart - Atrial Appendage|Heart - Left Ventricle', case=False, na=False)\n ]\n\nif not heart_related_scores.empty:\n print(f\"\\nFound {len(heart_related_scores)} heart-related entries.\")\n print(\"Heart-related scores head:\\n\", heart_related_scores.head())\n\n # Step 3.2: Identify the gene(s) with the most significant scores\n # Sort by absolute quantile_score or raw_score in descending order\n heart_related_scores['abs_quantile_score'] = heart_related_scores['quantile_score'].abs()\n heart_related_scores_sorted = heart_related_scores.sort_values(by='abs_quantile_score', ascending=False)\n\n # Filter to get top N unique genes for heart\n top_n = 5\n top_genes = heart_related_scores_sorted.dropna(subset=['gene_name']).head(top_n)\n\n if not top_genes.empty:\n print(f\"\\nTop {top_n} genes with most significant scores in heart tissue:\")\n for index, row in top_genes.iterrows():\n print(f\"Gene: {row['gene_name']}, Raw Score: {row['raw_score']:.4f}, Quantile Score: {row['quantile_score']:.4f}, Biosample: {row['biosample_name']} ({row['gtex_tissue']}), Assay: {row['Assay title']}\")\n causal_gene = top_genes['gene_name'].iloc[0] # Take the top gene as the most likely causal\n\n print(f\"\\nPotential causal gene for Hypoalphalipoproteinemia in heart tissue: {causal_gene}\")\n else:\n print(\"\\nNo gene names found in the top significant heart-related scores.\")\n # If no gene_name, try to find the most impactful entry and report its details\n most_impactful_entry = heart_related_scores_sorted.head(1)\n if not most_impactful_entry.empty:\n print(\"\\nMost impactful heart-related entry (no gene name found directly):\")\n print(most_impactful_entry[['variant_id', 'raw_score', 'quantile_score', 'biosample_name', 'gtex_tissue', 'Assay title']])\nelse:\n print(\"\\nNo heart-related scores identified in the dataset after broader search.\")\n print(\"Cannot determine causal gene without relevant tissue data.\")"
|
| 60 |
+
}
|
| 61 |
+
],
|
| 62 |
+
"observations": [],
|
| 63 |
+
"errors": [],
|
| 64 |
+
"final_solution": {
|
| 65 |
+
"step": 5,
|
| 66 |
+
"type": "solution",
|
| 67 |
+
"timestamp": "2025-10-07T15:43:40.034209",
|
| 68 |
+
"solution": "The analysis identified **APOA1** as the potential causal gene for the variant chr11:116837649:T>G, associated with Hypoalphalipoproteinemia, specifically impacting gene expression in heart tissue.\n\n**Summary of steps taken:**\n1. A VCF file named `variant.vcf` was created for the given variant (chr11:116837649:T>G).\n2. The `score_variants_batch` function was used to score the variant's impact, focusing on RNA-seq, CAGE, and ProCAP assays, which are relevant to gene expression. The output was saved to `variant_scores.csv`.\n3. The `variant_scores.csv` file was loaded and filtered to identify entries related to heart tissue using keywords and relevant ontology CURIEs.\n4. Within the heart-related entries, the genes with the most significant absolute `quantile_score` were identified.\n\n**Key Results:**\n* The variant shows significant impact on gene expression in heart tissues.\n* The top genes with the most significant absolute scores in heart tissue were:\n * **APOA1** (Raw Score: -1.7928, Quantile Score: -1.0000) in heart right ventricle, for polyA plus RNA-seq.\n * **APOA4** (Raw Score: -0.2483, Quantile Score: -1.0000) in right cardiac atrium, for total RNA-seq.\n * **APOA4** (Raw Score: -0.2264, Quantile Score: -1.0000) in left cardiac atrium, for total RNA-seq.\n * **APOA1** (Raw Score: -1.8555, Quantile Score: -1.0000) in left cardiac atrium, for total RNA-seq.\n * **APOA4** (Raw Score: -0.2401, Quantile Score: -1.0000) in heart right ventricle, for polyA plus RNA-seq.\n\nBased on these results, **APOA1** is the leading candidate for the causal gene, showing the largest absolute scores in heart tissue. Both APOA1 and APOA4 are well-known genes involved in lipid metabolism, which aligns with Hypoalphalipoproteinemia."
|
| 69 |
+
},
|
| 70 |
+
"timestamp": "2025-10-07T15:43:40.039797"
|
| 71 |
+
}
|