Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,12 @@ The architecture divides into `vtcode-core` (reusable library) and `src/` (CLI e
```rust
let client = McpClient::new("ws://localhost:8080");
let docs = client.call("get-library-docs", json!({
"context7CompatibleLibraryID": "/tokio/docs",
"library_id": "/tokio/docs",
"tokens": 5000,
"topic": "async runtime"
})).await?;
```
Discovers tools dynamically (e.g., `mcp_resolve-library-id` for Context7 IDs, `mcp_sequentialthinking` for chain-of-thought reasoning with branch/revision support, `mcp_get_current_time` for timezone-aware ops). Connection pooling and failover for multi-provider setups.
Discovers tools dynamically (e.g., `mcp_resolve-library-id` for provider-specific IDs, `mcp_sequentialthinking` for chain-of-thought reasoning with branch/revision support, `mcp_get_current_time` for timezone-aware ops). Connection pooling and failover for multi-provider setups.

### CLI Execution (`src/`)

Expand Down
5 changes: 0 additions & 5 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@
sse_servers = [ ]
shttp_servers = [ ]

[[mcp.stdio_servers]]
name = "context7"
command = "npx"
args = [ "-y", "@upstash/context7-mcp@latest" ]

[[mcp.stdio_servers]]
name = "time"
command = "uvx"
Expand Down
28 changes: 20 additions & 8 deletions docs/guides/mcp-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ allowlists control tool access, and how to troubleshoot common configuration iss
For HTTP transports, specify the endpoint and headers in place of the stdio fields. The
configuration loader automatically deserializes either transport variant.

To keep Context7 available without enabling it by default, add a disabled entry you can toggle
later:

```toml
[[mcp.providers]]
name = "context7"
enabled = false
command = "npx"
args = ["-y", "@upstash/context7-mcp"]
max_concurrent_requests = 3
```

## Allowlist Behaviour

MCP access is gated by pattern-based allowlists. The defaults apply to every provider unless the
Expand All @@ -59,13 +71,13 @@ broader default patterns.
[mcp.allowlist.default]
resources = ["docs/*"]

[mcp.allowlist.providers.context7]
[mcp.allowlist.providers.knowledge_base]
resources = ["journals/*"]
```

In this configuration:

- `context7` can access only `journals/*` resources.
- `knowledge_base` can access only `journals/*` resources.
- Other providers continue to match `docs/*` through the default rule.

## Testing the Integration
Expand All @@ -77,23 +89,23 @@ wiring:
cargo test -p vtcode-core mcp -- --nocapture
```

The suite includes mocked clients and parsing tests so it does not require live MCP servers. For
an end-to-end check against the Context7 MCP server, invoke the ignored smoke test which spawns the
official `@upstash/context7-mcp` package on demand:
The suite includes mocked clients and parsing tests so it does not require live MCP servers. For an
end-to-end check against a live provider, enable the ignored time-server smoke test once
`mcp-server-time` is installed locally:

```bash
cargo test -p vtcode-core --test mcp_context7_manual context7_list_tools_smoke -- --ignored --nocapture
cargo test -p vtcode-core --test mcp_integration_e2e test_time_mcp_server_integration -- --ignored --nocapture
```

Expect the test to take a little longer on the first run while `npx` downloads the server bundle.
Expect the test to take a little longer on the first run while the server binary is downloaded.

## Troubleshooting

- **Unexpected tool execution permissions** – confirm whether the provider defines its own
allowlist. Provider rules now override defaults, so missing patterns may block tools that defaults
would otherwise allow.
- **Provider handshake visibility** – VT Code now sends explicit MCP client metadata and
normalizes structured tool responses. Context7 results surface as plain JSON objects in the
normalizes structured tool responses. Provider results surface as plain JSON objects in the
tool panel so downstream renderers can display status, metadata, and message lists without
additional post-processing.
- **Stale configuration values** – ensure `max_concurrent_connections`, `request_timeout_seconds`,
Expand Down
124 changes: 123 additions & 1 deletion src/agent/runloop/tool_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
if let Some(tool) = tool_name {
if tool.starts_with("mcp_context7") {
render_mcp_context7_output(renderer, val)?;
} else if tool.starts_with("mcp_sequentialthinking") {
render_mcp_context7_output(renderer, val)?;
} else if tool.starts_with("mcp_sequentialthinking") {
render_mcp_sequential_output(renderer, val)?;
}
Expand Down Expand Up @@ -108,6 +110,98 @@
let plan: TaskPlan =
serde_json::from_value(plan_value).context("Plan tool returned malformed plan payload")?;

fn render_mcp_context7_output(renderer: &mut AnsiRenderer, val: &Value) -> Result<()> {

Check warning on line 113 in src/agent/runloop/tool_output.rs

View workflow job for this annotation

GitHub Actions / Cargo test

function `render_mcp_context7_output` is never used
let status = val
.get("status")
.and_then(|value| value.as_str())
.unwrap_or("unknown");

let meta = val.get("meta").and_then(|value| value.as_object());
let provider = val
.get("provider")
.and_then(|value| value.as_str())
.unwrap_or("context7");
let tool_used = val
.get("tool")
.and_then(|value| value.as_str())
.unwrap_or("context7");

renderer.line(
MessageStyle::Tool,
&format!("[{}:{}] status: {}", provider, tool_used, status),
)?;

if let Some(meta) = meta {
if let Some(query) = meta.get("query").and_then(|value| value.as_str()) {
renderer.line(
MessageStyle::ToolDetail,
&format!("┇ query: {}", shorten(query, 160)),
)?;
}
if let Some(scope) = meta.get("scope").and_then(|value| value.as_str()) {
renderer.line(MessageStyle::ToolDetail, &format!("┇ scope: {}", scope))?;
}
if let Some(limit) = meta.get("max_results").and_then(|value| value.as_u64()) {
renderer.line(
MessageStyle::ToolDetail,
&format!("┇ max_results: {}", limit),
)?;
}
}

if let Some(messages) = val.get("messages").and_then(|value| value.as_array())
&& !messages.is_empty()
{
renderer.line(MessageStyle::ToolDetail, "┇ snippets:")?;
for message in messages.iter().take(3) {
if let Some(content) = message.get("content").and_then(|value| value.as_str()) {
renderer.line(
MessageStyle::ToolDetail,
&format!("┇ · {}", shorten(content, 200)),
)?;
}
}
if messages.len() > 3 {
renderer.line(
MessageStyle::ToolDetail,
&format!("┇ · … {} more", messages.len() - 3),
)?;
}
}

if let Some(errors) = val.get("errors").and_then(|value| value.as_array())
&& !errors.is_empty()
{
renderer.line(MessageStyle::Error, "┇ provider errors:")?;
for err in errors.iter().take(2) {
if let Some(msg) = err.get("message").and_then(|value| value.as_str()) {
renderer.line(MessageStyle::Error, &format!("┇ · {}", shorten(msg, 160)))?;
}
}
if errors.len() > 2 {
renderer.line(
MessageStyle::Error,
&format!("┇ · … {} more", errors.len() - 2),
)?;
}
}

if let Some(input) = val.get("input").and_then(|value| value.as_object())
&& let Some(name) = input.get("LibraryName").and_then(|value| value.as_str())
{
let candidate = name.trim();
if !candidate.is_empty() {
let lowered = candidate.to_lowercase();
if lowered != "tokio" && levenshtein(&lowered, "tokio") <= 2 {
renderer.line(MessageStyle::Info, "┇ suggestion: did you mean 'tokio'?")?;
}
}
}

renderer.line(MessageStyle::ToolDetail, "┗ context7 lookup complete")?;
Ok(())
}

renderer.line(
MessageStyle::Output,
&format!(
Expand Down Expand Up @@ -218,6 +312,34 @@
if !candidate.is_empty() {
let lowered = candidate.to_lowercase();
if lowered != "tokio" && levenshtein(&lowered, "tokio") <= 2 {
fn levenshtein(a: &str, b: &str) -> usize {

Check warning on line 315 in src/agent/runloop/tool_output.rs

View workflow job for this annotation

GitHub Actions / Cargo test

function `levenshtein` is never used
let a_len = a.chars().count();
let b_len = b.chars().count();
if a_len == 0 {
return b_len;
}
if b_len == 0 {
return a_len;
}

let mut prev: Vec<usize> = (0..=b_len).collect();
let mut current = vec![0; b_len + 1];

for (i, a_ch) in a.chars().enumerate() {
current[0] = i + 1;
for (j, b_ch) in b.chars().enumerate() {
let cost = if a_ch == b_ch { 0 } else { 1 };
current[j + 1] = std::cmp::min(
std::cmp::min(current[j] + 1, prev[j + 1] + 1),
prev[j] + cost,
);
}
prev.copy_from_slice(&current);
}

prev[b_len]
}

renderer.line(MessageStyle::Info, "┇ suggestion: did you mean 'tokio'?")?;
}
}
Expand Down Expand Up @@ -1028,7 +1150,7 @@
fn non_terminal_tools_do_not_apply_special_styles() {
let git = GitStyles::new();
let ls = LsStyles::from_components(HashMap::new(), Vec::new());
let styled = select_line_style(Some("context7"), "+added", &git, &ls);
let styled = select_line_style(Some("knowledge_base"), "+added", &git, &ls);
assert!(styled.is_none());
}

Expand Down
12 changes: 6 additions & 6 deletions vtcode-core/src/config/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -577,10 +577,10 @@ mod tests {
provider_rules.tools = Some(vec!["list_*".to_string()]);
config
.providers
.insert("context7".to_string(), provider_rules);
.insert("knowledge".to_string(), provider_rules);

assert!(config.is_tool_allowed("context7", "list_documents"));
assert!(!config.is_tool_allowed("context7", "get_current_time"));
assert!(config.is_tool_allowed("knowledge", "list_documents"));
assert!(!config.is_tool_allowed("knowledge", "get_current_time"));
assert!(config.is_tool_allowed("other", "get_timezone"));
assert!(!config.is_tool_allowed("other", "list_documents"));
}
Expand Down Expand Up @@ -624,10 +624,10 @@ mod tests {
provider_rules.resources = Some(vec!["journals/*".to_string()]);
config
.providers
.insert("context7".to_string(), provider_rules);
.insert("knowledge".to_string(), provider_rules);

assert!(config.is_resource_allowed("context7", "journals/2024"));
assert!(!config.is_resource_allowed("context7", "docs/manual"));
assert!(config.is_resource_allowed("knowledge", "journals/2024"));
assert!(!config.is_resource_allowed("knowledge", "docs/manual"));
assert!(config.is_resource_allowed("other", "docs/reference"));
assert!(!config.is_resource_allowed("other", "journals/2023"));
}
Expand Down
5 changes: 1 addition & 4 deletions vtcode-core/src/core/agent/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,10 +344,7 @@ impl AgentRunner {
parallel_tool_config: Some(
crate::llm::provider::ParallelToolConfig::anthropic_optimized(),
),
reasoning_effort: if self
.provider_client
.supports_reasoning_effort(&self.model)
{
reasoning_effort: if self.provider_client.supports_reasoning_effort(&self.model) {
self.reasoning_effort
} else {
None
Expand Down
4 changes: 2 additions & 2 deletions vtcode-core/src/tools/registry/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -788,8 +788,8 @@ mod tests {
"sequentialthinking"
);
assert_eq!(
normalize_mcp_tool_identifier("Context7.Lookup"),
"context7lookup"
normalize_mcp_tool_identifier("Knowledge.Lookup"),
"knowledgelookup"
);
assert_eq!(normalize_mcp_tool_identifier("alpha_beta"), "alphabeta");
}
Expand Down
35 changes: 0 additions & 35 deletions vtcode-core/tests/mcp_context7_manual.rs

This file was deleted.

14 changes: 7 additions & 7 deletions vtcode-core/tests/mcp_integration_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,10 @@ args = ["mcp-server-time"]
max_concurrent_requests = 1

[[mcp.providers]]
name = "context7"
name = "knowledge-base"
enabled = true
command = "npx"
args = ["-y", "@upstash/context7-mcp@latest"]
args = ["-y", "@example/knowledge-mcp@latest"]
max_concurrent_requests = 2

[[mcp.providers]]
Expand All @@ -219,11 +219,11 @@ max_concurrent_requests = 1
assert!(time_provider.enabled);
assert_eq!(time_provider.max_concurrent_requests, 1);

// Check second provider (context7)
let context7_provider = &config.mcp.providers[1];
assert_eq!(context7_provider.name, "context7");
assert!(context7_provider.enabled);
assert_eq!(context7_provider.max_concurrent_requests, 2);
// Check second provider (knowledge-base)
let knowledge_provider = &config.mcp.providers[1];
assert_eq!(knowledge_provider.name, "knowledge-base");
assert!(knowledge_provider.enabled);
assert_eq!(knowledge_provider.max_concurrent_requests, 2);

// Check third provider (disabled)
let disabled_provider = &config.mcp.providers[2];
Expand Down
Loading
Loading