Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ permissions: {}
env:
CARGO_TERM_COLOR: always
BINARY_NAME: konnect
# Embedded into get_installation_info so release artifacts identify the
# exact source commit even when built outside a Git working tree.
KONNECT_BUILD_COMMIT: ${{ github.sha }}

jobs:
build:
Expand Down
12 changes: 6 additions & 6 deletions DEV.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Konnect/
│ │ ├── router/
│ │ │ ├── mod.rs # ToolRouter: load/unload toolsets
│ │ │ ├── registry.rs # Static toolset metadata + tools_for() dispatcher
│ │ │ └── meta_tools.rs # 6 always-visible meta-tools
│ │ │ └── meta_tools.rs # 7 always-visible meta-tools
│ │ └── tools/
│ │ ├── mod.rs # ToolDef, ToolContext, tool! macro, helpers, kicad_config_dir()
│ │ ├── cli.rs # kicad-cli v10 subprocess wrapper (verified against actual binary)
Expand Down Expand Up @@ -316,9 +316,9 @@ Source: [`crates/konnect-core/src/observability.rs`](crates/konnect-core/src/obs

## Tool Routing (Starter Kit + On-Demand Loading)

The server does NOT expose all 221 tools (227 total with the 6 meta-tools) in `tools/list` by default — that would cost ~23K tokens of context on every listing. Instead:
The server does NOT expose all 221 tools (228 total with the 7 meta-tools) in `tools/list` by default — that would cost ~23K tokens of context on every listing. Instead:

- **Startup**: only `STARTER_KIT` toolsets are pre-loaded (see `router/registry.rs::STARTER_KIT`). Currently: `project`, `config`. Combined with the 6 meta-tools, baseline `tools/list` is 20 tools ≈ 2K tokens.
- **Startup**: only `STARTER_KIT` toolsets are pre-loaded (see `router/registry.rs::STARTER_KIT`). Currently: `project`, `config`. Combined with the 7 meta-tools, baseline `tools/list` is 21 tools ≈ 2K tokens.
- **On demand**: the LLM reads `list_toolboxes` → calls `load_toolset(name)` to expose a toolset's tools in subsequent `tools/list` responses. `unload_toolset(name)` prunes them when the task shifts.
- **`tools/list_changed` notification**: sent on every load/unload so MCP clients refresh their local tool cache.
- **Error recovery**: if the LLM calls an unloaded tool, `handler.rs` returns an actionable error naming the toolset that owns it (so the LLM can load it and retry in one hop — no extra `list_toolboxes` round-trip).
Expand Down Expand Up @@ -391,9 +391,9 @@ convention for other `kicad-cli`-calling code.

## Current Stats

- **20 toolsets, 221 tools** + 6 meta-tools (4 routing + 2 observability — see `tool-directory.md`)
- Baseline `tools/list`: 20 tools / ~2K tokens (starter kit + meta-tools)
- Full-catalog `tools/list` (all loaded): 227 tools (221 registered + 6 meta) / ~25K tokens
- **20 toolsets, 221 tools** + 7 meta-tools (4 routing + 2 observability + 1 runtime diagnostic — see `tool-directory.md`)
- Baseline `tools/list`: 21 tools / ~2K tokens (starter kit + meta-tools)
- Full-catalog `tools/list` (all loaded): 228 tools (221 registered + 7 meta) / ~25K tokens
- **0 IPC stubs** (all protobuf methods implemented)
- **0 unimplemented tools**
- **Specctra DSN/SES are PCB-editor operations**, not `kicad-cli` commands.
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,13 @@ argument Konnect does not recognise is an error rather than being ignored, so a
typo such as `--cleint codex` stops instead of quietly installing for the
default client.

To verify which Konnect process an MCP client is actually using, call the
always-visible `get_installation_info` tool. It reports the serving build,
executable path, verified installation source when one can be proven, KiCad
CLI and IPC detection, and restart guidance. A missing build commit or an
`unknown` installation source means the available evidence was insufficient;
it is not silently guessed from a directory name.

### macOS

The [Releases](https://github.com/mixelpixx/Konnect/releases) page ships
Expand Down
91 changes: 91 additions & 0 deletions crates/konnect-core/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use std::env;
use std::fs;
use std::path::{Path, PathBuf};

fn main() {
println!("cargo:rerun-if-env-changed=KONNECT_BUILD_COMMIT");

let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("Cargo sets CARGO_MANIFEST_DIR");
let repo_root = Path::new(&manifest_dir)
.join("../..")
.canonicalize()
.unwrap_or_else(|_| Path::new(&manifest_dir).join("../.."));

let (commit, source) = match env::var("KONNECT_BUILD_COMMIT")
.ok()
.filter(|value| is_commit_id(value))
{
Some(commit) => (Some(commit), "build_environment"),
None => (commit_from_git_files(&repo_root), "git_head"),
};

if let Some(commit) = commit {
println!("cargo:rustc-env=KONNECT_BUILD_COMMIT={commit}");
println!("cargo:rustc-env=KONNECT_BUILD_COMMIT_SOURCE={source}");
}
}

/// Read Git's public repository metadata directly so source builds do not
/// depend on a `git` executable being available to Cargo build scripts.
fn commit_from_git_files(repo_root: &Path) -> Option<String> {
let git_dir = resolve_git_dir(repo_root)?;
let head_path = git_dir.join("HEAD");
println!("cargo:rerun-if-changed={}", head_path.display());
let head = fs::read_to_string(&head_path).ok()?;
let head = head.trim();
if is_commit_id(head) {
return Some(head.to_string());
}

let reference = head.strip_prefix("ref: ")?.trim();
let common_dir = resolve_common_dir(&git_dir);
for root in [&git_dir, &common_dir] {
let reference_path = root.join(reference);
println!("cargo:rerun-if-changed={}", reference_path.display());
if let Ok(value) = fs::read_to_string(&reference_path) {
let value = value.trim();
if is_commit_id(value) {
return Some(value.to_string());
}
}
}

let packed_refs = common_dir.join("packed-refs");
println!("cargo:rerun-if-changed={}", packed_refs.display());
let packed = fs::read_to_string(packed_refs).ok()?;
packed.lines().find_map(|line| {
let (commit, name) = line.split_once(' ')?;
(name == reference && is_commit_id(commit)).then(|| commit.to_string())
})
}

fn resolve_git_dir(repo_root: &Path) -> Option<PathBuf> {
let dot_git = repo_root.join(".git");
if dot_git.is_dir() {
return Some(dot_git);
}
let pointer = fs::read_to_string(dot_git).ok()?;
let value = pointer.trim().strip_prefix("gitdir: ")?;
let path = PathBuf::from(value);
Some(if path.is_absolute() {
path
} else {
repo_root.join(path)
})
}

fn resolve_common_dir(git_dir: &Path) -> PathBuf {
let Ok(value) = fs::read_to_string(git_dir.join("commondir")) else {
return git_dir.to_path_buf();
};
let path = PathBuf::from(value.trim());
if path.is_absolute() {
path
} else {
git_dir.join(path)
}
}

fn is_commit_id(value: &str) -> bool {
(7..=64).contains(&value.len()) && value.bytes().all(|byte| byte.is_ascii_hexdigit())
}
1 change: 1 addition & 0 deletions crates/konnect-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub mod mcp;
pub(crate) mod native_specctra_bridge;
pub mod observability;
pub mod router;
pub(crate) mod runtime_info;
pub(crate) mod specctra;
pub(crate) mod specctra_ses;
pub mod tools;
25 changes: 23 additions & 2 deletions crates/konnect-core/src/router/meta_tools.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! The 6 always-visible meta-tools.
//! The 7 always-visible meta-tools.
//!
//! Discovery / routing:
//! list_toolboxes() — show every toolset with descriptions and load state
Expand All @@ -9,6 +9,7 @@
//! Observability:
//! get_recent_calls(limit?) — last N tool calls (newest first) with timing + status
//! server_stats() — uptime, per-tool totals/errors, JSONL log path
//! get_installation_info() — serving build, binary, install, KiCad, and IPC provenance
//!
//! At server startup only the STARTER_KIT (`project`, `config`) is pre-loaded so
//! baseline context stays small. The LLM reads `list_toolboxes` and calls
Expand All @@ -19,7 +20,7 @@ use crate::mcp::protocol::{CallToolResult, McpToolDescription};
use crate::tools::ToolContext;
use serde_json::{json, Value};

/// Return the 6 meta-tool MCP descriptions (always in the tools/list response).
/// Return the 7 meta-tool MCP descriptions (always in the tools/list response).
pub fn meta_tool_descriptions() -> Vec<McpToolDescription> {
vec![
McpToolDescription {
Expand Down Expand Up @@ -120,6 +121,20 @@ pub fn meta_tool_descriptions() -> Vec<McpToolDescription> {
"required": []
}),
},
McpToolDescription {
name: "get_installation_info".to_string(),
description:
"Report read-only provenance for the Konnect process serving this call: build \
version and commit when available, executable path, conservatively detected \
install source, on-disk binary version, KiCad CLI version, redacted IPC \
endpoint, proven newer-binary state, and platform-specific restart guidance."
.to_string(),
input_schema: json!({
"type": "object",
"properties": {},
"required": []
}),
},
]
}

Expand All @@ -136,6 +151,7 @@ pub async fn handle_meta_tool(
"get_active_toolsets" => Some(handle_get_active_toolsets(ctx).await),
"get_recent_calls" => Some(handle_get_recent_calls(args, ctx).await),
"server_stats" => Some(handle_server_stats(ctx).await),
"get_installation_info" => Some(handle_get_installation_info(ctx).await),
_ => None,
}
}
Expand Down Expand Up @@ -293,6 +309,11 @@ async fn handle_server_stats(ctx: &std::sync::Arc<ToolContext>) -> CallToolResul
CallToolResult::json(&snap)
}

async fn handle_get_installation_info(ctx: &std::sync::Arc<ToolContext>) -> CallToolResult {
let info = crate::runtime_info::collect(&ctx.config).await;
CallToolResult::json(&info)
}

async fn handle_get_active_toolsets(ctx: &std::sync::Arc<ToolContext>) -> CallToolResult {
let active = ctx.router.active_names().await;
let all = ctx.router.all_toolsets();
Expand Down
Loading
Loading