Skip to content

Commit b9f114a

Browse files
authored
Merge pull request #52 from syncable-dev/feature/langgraph-integration
Feature/langgraph integration
2 parents e6a88a1 + 5f18084 commit b9f114a

6 files changed

Lines changed: 196 additions & 9 deletions

File tree

mcp-python-server-client/pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ description = "Demo examples to use for AI Agents with MCP"
55
readme = "README.md"
66
requires-python = ">=3.11"
77
dependencies = [
8+
"agno>=2.0.4",
89
"langchain>=0.3.25",
910
"langchain-mcp-adapters>=0.1.7",
1011
"langchain-openai>=0.3.21",
1112
"langgraph>=0.4.8",
1213
"mcp[cli]>=1.9.3",
1314
"python-dotenv>=1.1.0",
1415
"rich>=13.0.0",
16+
"uvicorn>=0.23.2",
17+
"pydantic>=2.0.0",
1518
]
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""An MCP client for the Agno SSE demo."""
2+
3+
import asyncio
4+
import os
5+
from dotenv import load_dotenv
6+
import openai
7+
8+
from agno.agent import Agent
9+
from agno.models.openai import OpenAIChat
10+
from agno.tools.mcp import MCPTools
11+
12+
load_dotenv()
13+
openai.api_key = os.getenv("OPENAI_API_KEY")
14+
15+
16+
async def main():
17+
"""Connects to an MCP server and uses it to run an agent."""
18+
server_url = "http://127.0.0.1:8008/sse"
19+
async with MCPTools(url=server_url, transport="sse") as mcp_tools:
20+
agent = Agent(model=OpenAIChat(id="gpt-4o"), tools=[mcp_tools])
21+
22+
prompts = [
23+
"Call the 'about_info' tool.",
24+
"Call 'analysis_scan' on path '../' with display 'matrix'.",
25+
"Call 'security_scan' on path '../'.",
26+
"Call 'dependency_scan' on path '../'.",
27+
]
28+
29+
for prompt in prompts:
30+
print(f"\n--- Prompt: {prompt} ---")
31+
await agent.aprint_response(prompt, stream=True)
32+
33+
34+
if __name__ == "__main__":
35+
asyncio.run(main())
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""A demo of the Agno MCP tools with a stdio transport."""
2+
3+
import asyncio
4+
import os
5+
from dotenv import load_dotenv
6+
import openai
7+
8+
9+
from agno.agent import Agent
10+
from agno.models.openai import OpenAIChat
11+
from agno.tools.mcp import MCPTools
12+
13+
14+
load_dotenv()
15+
openai.api_key = os.getenv("OPENAI_API_KEY")
16+
17+
18+
async def main():
19+
"""Fetches tools from a subprocess and uses them to run an agent."""
20+
async with MCPTools(command="uv run mcp-stdio") as mcp_tools:
21+
agent = Agent(model=OpenAIChat(id="gpt-4.1"), tools=[mcp_tools])
22+
23+
prompts = [
24+
"Call the 'about_info' tool.",
25+
"Call 'analysis_scan' on path '../' with display 'matrix'.",
26+
"Call 'security_scan' on path '../'.",
27+
"Call 'dependency_scan' on path '../'.",
28+
]
29+
30+
for prompt in prompts:
31+
print(f"\n--- Prompt: {prompt} ---")
32+
await agent.aprint_response(prompt, stream=True)
33+
34+
35+
if __name__ == "__main__":
36+
asyncio.run(main())

mcp-python-server-client/uv.lock

Lines changed: 73 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust-mcp-server-syncable-cli/Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ rust-mcp-sdk = "0.6.3"
3131
tracing = "0.1.41"
3232
tracing-subscriber = { version = "0.3.19", features = ["fmt", "env-filter"] }
3333
serde = { version = "1.0", features = ["derive"] }
34-
serde_json = "1.0"
34+
serde_json = "1.0.145"
3535
tokio = { version = "1", features = ["full"] }
3636
env_logger = "0.11"
37-
syncable-cli = "0.18.2"
37+
syncable-cli = "0.18.3"
3838
axum = { version = "0.8.4", features = ["json"] }
3939
futures = "0.3.31"
4040
bytes = "1.10.1"
@@ -55,7 +55,7 @@ tempfile = "3"
5555
proptest = "1"
5656
serde_json = "1.0"
5757
reqwest = { version = "0.12", features = ["blocking"] }
58-
http = "0.2"
58+
http = "1.3.1"
5959
tokio-test = "0.4.4"
6060

6161
[profile.release]

rust-mcp-server-syncable-cli/src/tools.rs

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,11 @@ pub struct SecurityScanTool {
148148
impl SecurityScanTool {
149149
pub fn call_tool(&self) -> Result<CallToolResult, CallToolError> {
150150
let project_path_str = self.path.as_deref().unwrap_or(".");
151+
152+
// Log to stderr so we don't interfere with MCP stdout JSON messages
153+
eprintln!("🔒 Scanning project for security: {}", project_path_str);
154+
eprintln!("➡️ Calling syncable_cli::handle_security...");
155+
151156
let security_results = syncable_cli::handle_security(
152157
Path::new(project_path_str).to_path_buf(),
153158
syncable_cli::cli::SecurityScanMode::Balanced,
@@ -157,7 +162,7 @@ impl SecurityScanTool {
157162
false,
158163
false,
159164
vec![],
160-
syncable_cli::cli::OutputFormat::Table,
165+
syncable_cli::cli::OutputFormat::Json,
161166
None,
162167
false,
163168
);
@@ -169,10 +174,26 @@ impl SecurityScanTool {
169174
e
170175
)
171176
});
172-
Ok(CallToolResult::text_content(vec![TextContent::new(json_output, None, None)]))
177+
178+
eprintln!("✅ handle_security returned ({} bytes)", json_output.len());
179+
180+
// Validate JSON to ensure it's well-formed
181+
match serde_json::from_str::<serde_json::Value>(&json_output) {
182+
Ok(_) => {
183+
eprintln!("✅ JSON validation passed");
184+
eprintln!("📤 Sending full response ({} bytes)", json_output.len());
185+
Ok(CallToolResult::text_content(vec![TextContent::new(json_output, None, None)]))
186+
}
187+
Err(e) => {
188+
eprintln!("⚠️ JSON validation failed: {}", e);
189+
eprintln!("First 500 chars: {}", &json_output[..std::cmp::min(500, json_output.len())]);
190+
return Err(CallToolError::new(AnalyzeToolError(format!("Invalid JSON response: {}", e))));
191+
}
192+
}
173193
}
174194
Err(e) => {
175195
let error_message = format!("Failed to analyze project for security: {}", e);
196+
eprintln!("❌ handle_security error: {}", &error_message);
176197
Err(CallToolError::new(AnalyzeToolError(error_message)))
177198
}
178199
}
@@ -191,13 +212,18 @@ pub struct DependencyScanTool {
191212
impl DependencyScanTool {
192213
pub async fn call_tool(&self) -> Result<CallToolResult, CallToolError> {
193214
let project_path_str = self.path.as_deref().unwrap_or(".");
215+
216+
// Log to stderr so we don't interfere with MCP stdout JSON messages
217+
eprintln!("📦 Scanning project for dependencies: {}", project_path_str);
218+
eprintln!("➡️ Calling syncable_cli::handle_dependencies...");
219+
194220
let dependency_results = syncable_cli::handle_dependencies(
195221
Path::new(project_path_str).to_path_buf(),
196222
false,
197223
false,
198224
false,
199225
false,
200-
syncable_cli::cli::OutputFormat::Table,
226+
syncable_cli::cli::OutputFormat::Json,
201227
)
202228
.await;
203229
match dependency_results {
@@ -208,10 +234,26 @@ impl DependencyScanTool {
208234
e
209235
)
210236
});
211-
Ok(CallToolResult::text_content(vec![TextContent::new(json_output, None, None)]))
237+
238+
eprintln!("✅ handle_dependencies returned ({} bytes)", json_output.len());
239+
240+
// Validate JSON to ensure it's well-formed
241+
match serde_json::from_str::<serde_json::Value>(&json_output) {
242+
Ok(_) => {
243+
eprintln!("✅ JSON validation passed");
244+
eprintln!("📤 Sending full response ({} bytes)", json_output.len());
245+
Ok(CallToolResult::text_content(vec![TextContent::new(json_output, None, None)]))
246+
}
247+
Err(e) => {
248+
eprintln!("⚠️ JSON validation failed: {}", e);
249+
eprintln!("First 500 chars: {}", &json_output[..std::cmp::min(500, json_output.len())]);
250+
return Err(CallToolError::new(AnalyzeToolError(format!("Invalid JSON response: {}", e))));
251+
}
252+
}
212253
}
213254
Err(e) => {
214255
let error_message = format!("Failed to analyze project for dependencies: {}", e);
256+
eprintln!("❌ handle_dependencies error: {}", &error_message);
215257
Err(CallToolError::new(AnalyzeToolError(error_message)))
216258
}
217259
}

0 commit comments

Comments
 (0)