Changelog¶
0.3.40 — 2026-05-29¶
Added — server::watch default skip-patterns¶
The watch primitive now drops events under conventional noise paths
(.git/, target/, node_modules/, __pycache__/, .venv/,
build/, dist/, .DS_Store) and noise extensions (.pyc, .pyo,
.swp, .swo, .tmp) before the debounced callback runs. A
wide-sandbox watcher under active development can generate hundreds
of FSEvents per second from cargo build / npm install / git
operations alone; without the filter every consumer either rebuilt
wastefully or implemented the same skip list. With it, consumers see
only events that could plausibly matter.
New API:
pub const DEFAULT_SKIP_SUBSTRINGS: &[&str] = &[
"/.git/", "/target/", "/node_modules/", "/__pycache__/",
"/.venv/", "/build/", "/dist/", "/.DS_Store",
];
pub const DEFAULT_SKIP_EXTENSIONS: &[&str] = &[
"pyc", "pyo", "swp", "swo", "tmp",
];
pub struct WatchConfig {
pub skip_substrings: Vec<String>,
pub skip_extensions: Vec<String>,
}
impl WatchConfig {
pub fn default() -> Self; // recommended; uses the DEFAULT_* sets above
pub fn unfiltered() -> Self; // empty skip set; every event reaches the callback
pub fn is_skipped(&self, path: &Path) -> bool;
}
pub fn watch_with_config(
dir: &Path,
on_change: Option<ChangeHandler>,
debounce: Option<Duration>,
config: WatchConfig,
) -> Result<WatchHandle>;
The existing pub fn watch(dir, on_change, debounce) is unchanged at
the signature level; internally it now delegates to
watch_with_config(..., WatchConfig::default()). Consumers that want
every event opt out via
watch_with_config(..., WatchConfig::unfiltered()).
Skip-substring matching is anchored with / on both sides where
appropriate so a file literally named target at the repo root
doesn’t false-match (but /repo/target/foo does). Matching is
allocation-free per event. Empty post-filter batches (a pure-noise
storm) skip the callback invocation entirely.
Why now¶
Originally surfaced by an MCP-servers operator deploying
kglite-mcp-server with a wide workspace.root
(/Volumes/EksternalHome/Koding, ~360k files). The cargo build
storm under that root generated ~120 FSEvents/sec; the downstream
kglite watch callback rebuilt the active code-tree on every event.
kglite shipped its own fix on 2026-05-25 (active-root scoping +
language_for_path filter + deferred rebuild — kglite main commits
6596da6 + 0306655), but the upstream server::watch is still
walking those events through the FFI / debouncer cycle before the
kglite filter drops them.
This change moves the skip to the source. Every server::watch
consumer benefits — kglite’s filter becomes defense-in-depth, any
future code-indexer-shaped consumer gets correct defaults, and the
mcp-methods binary’s own log-changes mode emits fewer noise lines.
Compatibility¶
Existing callers see strictly fewer callback invocations for paths
under the skip set. Any consumer relying on .git/objects/ writes
hitting their callback has a bug already. No changes to
watch(dir, on_change, debounce)’s signature; downstream kglite
needs no source change to adopt — just bumps its mcp-methods pin
when 0.3.40 ships.
Unit tests cover the default skip set, the unfiltered escape hatch,
custom skip configs, anchoring (target file vs target/ dir), a
positive end-to-end “callback fires on a real file change” test, and
the debouncer’s batch-retention decision (retain_unskipped) — a
noise-only batch retains nothing (so no callback fires), a mixed batch
keeps only non-noise paths. The retention decision is tested as a pure
function rather than against a live watcher, since inotify (Linux) and
FSEvents (macOS) report different event paths for the same writes.
Fixed — graph_overview against kglite 0.10¶
fastmcp.register_overview forwarded a limit= kwarg to
graph.describe(), which kglite dropped from KnowledgeGraph.describe()
in the 0.10 line. Every graph_overview() call against a kglite ≥ 0.10
graph raised TypeError: describe() got an unexpected keyword argument 'limit' — a broken first-reach escape-hatch tool for any consumer on
the current kglite. The limit parameter is removed; describe()
already adapts its output to graph scale, so no replacement knob is
needed. The test/example graph stubs now mirror the post-0.10
describe(types, connections) signature, so a regression that
re-introduces limit forwarding fails the suite instead of passing
against an over-permissive mock. Reported by kglite-docs (kglite 0.10.5,
mcp-methods 0.3.39).
Fixed — cypher_query text mode returns a non-string¶
fastmcp.register_cypher_query returned graph.cypher(query) raw in
text mode. kglite’s cypher() returns a lazy ResultView (Polars-backed,
not a str), so the -> str tool handed a non-string to FastMCP’s
output validation and the call failed. The text path now coerces with
str() — ResultView.__str__ renders the result table — matching the
defensive coercion the CSV path already did. Graph impls that already
return a str pass through unchanged. Flagged by kglite-docs alongside
the graph_overview report.
0.3.39 — 2026-05-22¶
Fixed — git_api doubled /repos/ for leading-slash paths¶
git_api_internal decided pass-through-vs-prefix by testing the path
against a top_level list (repos/, search/, …) whose entries have no
leading slash. A path written in the idiomatic absolute form the GitHub
REST docs render — /repos/owner/name, /search/issues?q=… — matched
none of them, fell into the relative branch, and got wrapped in
/repos/<repo>/, producing a doubled prefix and a 404:
git_api("someorg/somerepo", "/repos/kkollsga/kglite")
actual: https://api.github.com/repos/someorg/somerepo//repos/kkollsga/kglite → 404
want: https://api.github.com/repos/kkollsga/kglite
The same call without the leading slash worked, which made the failure
surprising and slow to diagnose. URL construction now strips a single
leading slash before the top_level check, so /repos/… and repos/…
(and /search/… vs search/…) are equivalent.
Reported by kglite (their issue #19), surfaced through kglite’s
github_api MCP tool against non-active repos.
Changed — URL construction lifted into build_git_api_url¶
The pure URL-building logic is now a build_git_api_url(repo, path)
helper that git_api_internal calls, so a regression test can assert on
the constructed URL without a network round-trip — a URL-building bug
with no unit coverage is the kind that silently comes back.
Tests¶
leading_slash_paths_normalise— leading and non-leading slash forms build the same URL for both top-level and relative paths.
0.3.38 — 2026-05-18¶
Added — Registry::from_manifest on the Rust crate¶
The one-shot manifest → resolved-skills-registry orchestration that previously lived only in mcp-methods-py’s SkillRegistry.from_manifest is now a public method on the Rust Registry builder:
let registry = mcp_methods::server::Registry::from_manifest(&manifest_path, true)?;
Loads the manifest, optionally merges framework bundled defaults, auto-detects the project layer at <basename>.skills/, layers in operator-declared skills: paths, and finalises in a single call. Use the builder directly for bespoke layering (e.g. supplying a SkillPredicateEvaluator via with_predicate_evaluator, or include_str!’d downstream bundled skills via add_bundled).
The pyo3 wheel’s SkillRegistry.from_manifest now delegates to this method — no public Python API change. Downstream pyo3 wrappers that consume mcp-methods from crates.io (kglite’s incoming kglite._mcp_internal wrapper crate, others) can now call the same library function instead of replicating the six-line orchestration, eliminating drift risk on future layering tweaks.
Added — SkillError::Manifest variant¶
Carries the manifest path + message when from_manifest fails at the manifest-load step (before any skill loading happens). Lets callers distinguish “your YAML is broken” from “your skills/ tree is broken” without parsing error messages.
Motivated by kglite as part of the “consume the Cargo crate, not the wheel” framing — by widening the library’s public surface, downstream wheels (kglite, others) can stay thin pyo3 wrappers instead of carrying business logic.
Tests¶
from_manifest_resolves_full_stack— happy path: bundled + project + operator-declared paths compose through the one-shot call.from_manifest_surfaces_manifest_load_error— broken manifest YAML surfaces asSkillError::Manifest, not a downstream parsing error.
0.3.37 — 2026-05-14¶
Fixed — agent retrieval gap (the load-bearing one)¶
The auto-inject pass for auto_inject_hint: true skills previously appended a [See prompts/get <name> for full methodology.] pointer to the matched tool’s description. Agents in real MCP clients can’t reach prompts/get — Claude Code, Claude Desktop, Cursor, and Continue all expose only tools/* to the model; the prompts/* plane was designed for human-invoked slash commands. The pointer was a dangling reference; operator-authored methodology was unreachable by the agent.
This release replaces the pointer with full-body embedding under a ## Methodology header in the matched tool’s description. Bounded by the existing 4 KB soft / 16 KB hard size caps the framework enforces per skill. Operators who want the older pointer-shaped behaviour (or no inject at all) set auto_inject_hint: false per skill.
prompts/list and prompts/get continue to work — they’re still useful for MCP clients that do surface prompts to the agent, plus CLI introspection (mcp-server skills-show). The auto-inject path just becomes the primary delivery channel for the agentic clients in use today.
Found by an mcp-servers operator deploying kglite 0.9.31 + mcp-methods 0.3.36 against a live Claude Code session. Their three eval queries passed only because graph_overview happened to inline schema + methodology in one tool response; skills with non-parallel property prefixes, multi-hop temporal idioms, or out-of-schema content would have failed silently.
Fixed — silent file-drop on YAML parse error¶
SkillRegistry.from_manifest previously silently skipped any SKILL.md whose frontmatter failed to parse. The intent was “one broken skill in a domain pack shouldn’t take down the rest” — but the surface was too silent: operators hitting an unquoted colon in a description (First clause: second clause, which PyYAML reads as a mapping value separator) spent 25-minute debug sessions wondering why their override “took” but didn’t show up in prompts/list.
Two-channel fix:
tracing::warn!continues to fire per dropped file. Operators with structured tracing get the warning immediately.New
ResolvedRegistry.parse_warnings()Rust getter andSkillRegistry.parse_warningsPython getter — durable structured surface that downstream binaries can render in their boot summary. ReturnsVec<ParseWarning>(Rust) orlist[dict](Python) with per-file path + error.
let registry = SkillRegistry::new()
.auto_detect_project_layer(&yaml_path)
.finalise()?;
for warning in registry.parse_warnings() {
eprintln!("skill drop: {} — {}", warning.path.display(), warning.error);
}
reg = SkillRegistry.from_manifest("./my_mcp.yaml")
for w in reg.parse_warnings():
print(f"skill drop: {w['path']} — {w['error']}")
Same pattern as the existing (no .env found) boot lines downstream binaries already render — operators internalised that channel.
Tests + docs¶
load_skills_from_dir_surfaces_yaml_parse_failure_as_warning— reproduces the operator’s colon-in-description scenario; good skill still loads, broken file surfaces as a structured warning.resolved_registry_parse_warnings_propagated_from_project_layer— end-to-end throughRegistry::finalise.serve_prompts_auto_injects_full_body_into_matching_tool— replaces the pre-0.3.37 pointer-assertion test; now asserts## Methodologyheader + body sentinel are present, and thatprompts/getis not referenced (anti-regression).docs/guides/writing-effective-skills.mdgains a “How skill bodies reach the agent” section explaining the retrieval-gap rationale and authoring implications.
Backwards compatibility¶
A skill without
auto_inject_hint(or withauto_inject_hint: true, the default) now produces a larger tool description than in 0.3.36. The size is bounded by the per-skill caps; operators who want the old behaviour setauto_inject_hint: false.parse_warningsis a new field onResolvedRegistry— additive, doesn’t break any existing usage.The
load_skills_from_dirfunction’s return type changed fromResult<Vec<Skill>, _>toResult<(Vec<Skill>, Vec<ParseWarning>), _>. This is technically a breaking change for anyone calling that function directly, but kglite’s audit and our own grep confirmed no downstream consumers reach for it — they go throughRegistry::finaliseinstead.
0.3.36 — 2026-05-14¶
Added — applies_when: predicate gating on individual skills¶
Skills can now declare predicate conditions for activation. Skills with applies_when: predicates that don’t evaluate to true are silently omitted from prompts/list and prompts/get. Inspired by kglite’s three-deployment scenario (legal / o&g / code) where the same skill catalogue ships to every deployment but only some skills should fire per-graph.
---
name: read_code_source
description: Resolve qualified_name → source slice. TRIGGER when ...
applies_when:
graph_has_node_type: [Function, Class]
graph_has_property: { node_type: Function, prop_name: module }
tool_registered: cypher_query
extension_enabled: csv_http_server
---
Bounded predicate set — not a DSL. All populated predicates are ANDed. Predicate categories:
Framework-internal —
tool_registered,extension_enabled. Dispatched againstServerOptions.{tool_router, extensions}without consulting any evaluator.Domain —
graph_has_node_type,graph_has_property. Dispatched via the newSkillPredicateEvaluatortrait. Downstream binaries register one withRegistry::with_predicate_evaluator(...)beforefinalise().
Evaluators that don’t recognise a clause return None; the framework records Unknown and treats it as inactive — safer than silently activating the wrong-domain skill.
Behaviour split: operator-facing vs agent-facing¶
Agent-facing
prompts/list/prompts/get— inactive skills are silently omitted.tracing::info!logs the failed clauses so the boot log carries full attribution.Operator-facing
mcp-server skills-list— shows every resolved skill with anactive/inactivecolumn and indented per-clause outcomes for inactive ones. Mirrors the existing loud-collision pattern.
Mirrors the existing split for workspace activation: operator sees full provenance, agent sees only the resolved choice.
ServerOptions.extensions field¶
ServerOptions gains an extensions: serde_json::Map<String, Value> field, populated from the manifest’s extensions: block by from_manifest. Empty map when no extensions: block is present.
Downstream binaries that previously read Manifest.extensions directly can now read the same data off ServerOptions without re-loading the manifest. The framework uses this internally for the extension_enabled: predicate.
Python surface — Skill.applies_when¶
mcp_methods.Skill gains an applies_when getter returning a dict | None. Python operators can pre-filter their registries before calling register_skills_as_prompts:
for skill in registry.skills():
if skill.applies_when and "graph_has_node_type" in skill.applies_when:
# filter against our domain state, skip if predicate fails
...
The Python SkillPredicateEvaluator trait is not surfaced in this release — operators who need domain predicate dispatch from Python should pre-filter manually using applies_when introspection. Native Python evaluator support requires a Python-callable → Rust-trait bridge that’s deferred to a future release.
Backwards compatibility¶
A SKILL.md without applies_when: is always active — every existing skill keeps working unchanged. The tool_registered and extension_enabled framework-internal predicates work without any evaluator wired in, so single-domain consumers get most of the predicate engine for free.
0.3.35 — 2026-05-14¶
Added — skills-aware MCP via the prompts/ namespace¶
A new top-level manifest field, skills:, opts a deployment into
shipping operator-authored methodology as MCP prompts. The three-layer
composition is project → domain pack → bundled defaults, with
later-layer (i.e. project) entries fully replacing same-named entries
in the lower layers:
name: kglite-mcp-server
skills:
- true # framework defaults (5 bundled skills)
- ./my-skills/ # domain skill-pack (operator-declared dir)
Polymorphic shape: skills: false (default), skills: true (bundled
only), skills: ./path/ (one domain pack), or a list mixing booleans
and paths. The auto-detected project layer is <basename>.skills/
adjacent to the manifest YAML — if present, it wins over both
domain-pack and bundled.
Bundled framework defaults ship five SKILL.md files (grep,
read_source, list_source, github_issues, repo_management) that
operators inherit when skills: true. Downstream binaries can add
their own bundled layer via Registry::add_bundled_many(...) before
finalising.
Wiring helpers for downstream binaries¶
Downstream binaries reach the wiring with roughly ten lines of glue:
let manifest = mcp_methods::server::load_manifest(&path)?;
let registry = mcp_methods::server::SkillRegistry::new()
.merge_framework_defaults()
.auto_detect_project_layer(&path)
.layer_dirs(&manifest.skills, &path)?
.finalise()?;
let mut server = mcp_methods::server::McpServer::new(opts);
mcp_methods::server::serve_prompts(®istry, &mut server);
serve_prompts(registry, server) populates the prompts/list /
prompts/get surface and applies an auto-injection pass on tool
descriptions: skills with auto_inject_hint: true whose name matches
a registered tool get a “see prompts/get <name> for full
methodology” pointer appended to that tool’s description, so agents
who only read tools/list can still find the methodology.
Zero impact on existing deployments¶
A manifest with no skills: declaration is a verbatim-current deploy:
no prompts/ capability advertised in get_info, no behavioural
diff. The constraint is enforced at the type level via
SkillsSource::Disabled (the default) and at the runtime level via
the empty prompt_router — the rmcp default list_prompts /
get_prompt impls return empty / not-found when no skills are
registered.
Python bindings¶
mcp_methods.SkillRegistry and mcp_methods.Skill ship as thin
pyo3 wrappers (newtype + delegate). SkillRegistry.from_manifest(path, include_bundled=True) builds the registry; register_skills_as_prompts(app, registry) in mcp_methods.fastmcp wires it into a FastMCP server in
one call. The framework crate (mcp-methods) remains pyo3-free; the
CI gate (cargo tree -p mcp-methods -e all | grep pyo3 returns empty)
is unchanged.
Frontmatter schema¶
Each SKILL.md file ships YAML frontmatter for metadata:
---
name: cypher_query
description: One-line summary for `prompts/list`.
applies_to:
mcp_methods: ">=0.3.35"
references_tools:
- cypher_query
references_arguments:
- cypher_query.format
auto_inject_hint: true # default
applies_when: [] # phase-3 predicate hooks; parsed but inert
---
# Methodology body in markdown...
name and description are required; everything else is optional.
The applies_when: predicate evaluator is deferred to a later release
— predicates parse but don’t gate yet.
Size limits¶
The registry enforces three soft/hard caps to keep the prompt surface healthy: 4 KB lint-warns, 16 KB hard-fails a single skill, 64 KB caps the resolved-set total. The framework’s bundled skills round-trip through CI tests that pin the limits.
0.3.34 — 2026-05-14¶
Added — tools[].bundled: rename: per-deployment tool aliasing¶
The bundled-override shape from 0.3.31 gains a third axis:
tools:
- bundled: cypher_query
rename: legal_cypher_query # expose to agent under a different name
description: "Cypher against the legal corpus knowledge graph."
- bundled: graph_overview
rename: legal_graph_overview
- bundled: ping
hidden: true
Use case: when an operator runs multiple downstream servers each
backed by a different data source (e.g. three kglite-mcp-servers
for open_source, legal, prospect), the bundled tool names
collide across servers and MCP ToolSearch results rank them
near-identically. The agent can’t disambiguate which cypher_query
to use. Rename lets each deployment expose distinct names —
open_source_cypher_query, legal_cypher_query,
prospect_cypher_query — so search ranking and tool selection
behave correctly.
The rename composes with the existing description: and hidden:
axes; an override can use any combination.
Schema¶
Field |
Type |
Default |
Notes |
|---|---|---|---|
|
string |
unset |
Replaces the bundled tool’s agent-facing name. Must be a valid identifier ( |
Permitted alongside bundled:, description:, hidden:. Forbidden
on tool-creation (cypher: / python:) entries — the override-only
constraint from 0.3.31 still applies.
Framework records, consumer enforces¶
The framework parses rename: and surfaces it via BundledOverride.rename
and Manifest::to_json(). It does not validate that the renamed
name collides with another tool (bundled, cypher, or python) in the
manifest — the framework doesn’t know the downstream binary’s full
bundled-tool catalogue, so collision detection has to live in the
consumer.
The kglite consumer landing in 0.9.30 does this check at boot: “rename doesn’t shadow any other registered tool” → exit cleanly with a clear error if it does. Same pattern as advisory trust gates: framework declares, consumer enforces.
JSON shape addition¶
Manifest::to_json() emits the new field on bundled entries:
{
"kind": "bundled",
"name": "cypher_query",
"rename": "legal_cypher_query", // null when not declared
"description": "...",
"hidden": false
}
Non-breaking under the existing JSON-shape stability guarantee —
consumers that ignore unknown keys keep working; consumers that
want rename support match on the rename key.
Rust API change (heads up)¶
BundledOverride struct gains a rename: Option<String> field.
Consumers destructuring this struct will get a compile error and
need to handle the new field:
// Before 0.3.34
let BundledOverride { name, description, hidden } = &override_spec;
// After 0.3.34 — compile error: field `rename` missing
let BundledOverride { name, description, hidden, rename } = &override_spec;
Non-exhaustive struct attribute isn’t applied — additions remain visible at consumer-build time, which is the right failure mode for adding new fields to a public struct.
Tests¶
Six new tests in manifest.rs::tests under the bundled_* family:
bundled_rename_parses_when_valid_identifier— happy pathbundled_rename_alongside_description_parses— composes withdescription:bundled_rename_defaults_to_none— omittingrename:produces Nonerejects_bundled_rename_with_invalid_identifier—123-badrejectedrejects_bundled_rename_with_non_string_value—42rejectedbundled_rename_serialises_to_json— JSON shape addition verified
123 mcp-methods unit tests pass (was 117 at 0.3.33; +6 net). 7 deployed_manifests + 7 mcp-server tests unchanged.
Origin¶
Reported by the mcp-servers operator’s post-0.9.29 ToolSearch audit: multiple kglite servers with identical tool surfaces caused ranking ambiguity. kglite implemented the framework half and pushed it to this tree; reviewed, no design changes needed.
0.3.33 — 2026-05-14¶
Changed — workspace.applies_to now accepts glob patterns + lists¶
The opt-in declaration introduced in 0.3.32 was a single literal path string. 0.3.33 widens it to three shapes:
workspace:
# 1. Literal pattern (existing in 0.3.32) — match this exact name
applies_to: ./repos
# 2. Glob pattern (NEW) — match a subset by naming convention
applies_to: ./prod-* # any child whose name starts with "prod-"
applies_to: ./* # any direct child (wildcard)
applies_to: ./test-? # one-char suffix variant
# 3. List of patterns (NEW) — match if any pattern matches
applies_to: [./repos, ./clones]
applies_to: [./prod-*, ./test-*] # multiple naming conventions
Glob syntax follows globset (the
ripgrep/cargo crate): * (any chars), ? (one char), [abc]
(character class), ** (any depth — irrelevant here since the
parent-walk is single-level).
The opt-in semantics are unchanged: when the parent-walk fallback
finds a manifest, the framework compares the actual workspace_dir’s
basename against the declared pattern(s). Match → use the manifest.
No match or no declaration → operator must pass --mcp-config
explicitly. The accidental-discovery footgun stays closed; this
release just widens what an author can opt into.
Use case¶
Operators with a repos/ sandbox containing many projects no
longer need to enumerate each child:
repos/
├── workspace_mcp.yaml # applies_to: ./*
├── project-alpha/ # --workspace repos/project-alpha/ ✓
├── project-beta/ # --workspace repos/project-beta/ ✓
└── client-acme/ # --workspace repos/client-acme/ ✓
Or filter by naming convention:
deployments/
├── workspace_mcp.yaml # applies_to: ./prod-*
├── prod-api/ # ✓
├── prod-web/ # ✓
├── stage-api/ # ✗ (parent-walk discovery refuses)
└── test-runner/ # ✗
Parse-time validation¶
Patterns are validated at boot via globset::Glob::new:
Empty patterns rejected (
applies_to: "")Multi-segment paths rejected (
applies_to: ./a/b/c— parent-walk is single-level, so deeper paths could never match)Parent-relative rejected (
applies_to: ..)Absolute paths rejected (
applies_to: /abs/foo)Invalid glob syntax rejected (e.g. unterminated
[)
Operators get a clear error at boot rather than silent non-matching.
Internal shape change¶
WorkspaceConfig::applies_to changed from Option<String> to
Option<AppliesTo> where:
pub enum AppliesTo {
Pattern(String), // single pattern
Patterns(Vec<String>), // multiple patterns
}
Leading ./ is stripped at parse time (storage is normalised).
This is a Rust-API surface change since 0.3.32; consumers that read
manifest.workspace.applies_to need to handle the enum.
JSON shape change¶
Manifest::to_json() now emits workspace.applies_to polymorphically:
Pattern("repos")→"repos"(string)Patterns(["repos", "clones"])→["repos", "clones"](array)None→null
Mirrors the YAML shape. Consumers should accept both types.
New dependency¶
Direct dependency on globset = "0.4" added to mcp-methods. The
crate was already a transitive dep via ignore; making it direct
just lets the manifest parser validate glob patterns at boot.
Tests¶
Six new tests in manifest.rs::tests:
find_workspace_applies_to_wildcard_matches_any_child—*matches three different child namesfind_workspace_applies_to_glob_matches_prefix—./prod-*matchesprod-api/prod-webbut nottest-*find_workspace_applies_to_list_matches_any_entry—[./repos, ./clones]matches either, rejects othersapplies_to_rejects_deep_path_at_parse_time—./a/b/cerrors at bootapplies_to_rejects_invalid_glob_at_parse_time—./[unterminatederrors at bootapplies_to_rejects_parent_relative—..and../upboth error
117 mcp-methods unit tests pass (was 111 at 0.3.32; +6 net). 7 deployed_manifests + 7 mcp-server tests unchanged.
Origin¶
Operator scenario raised by Kristian: a repos/ directory holding
many child repos, with one manifest covering them all. Single-path
match didn’t cover this; glob + list shapes do.
0.3.32 — 2026-05-14¶
Changed — find_workspace_manifest parent-walk fallback (opt-in)¶
find_workspace_manifest(workspace_dir) now also looks one level up
for workspace_mcp.yaml, but only when the parent manifest declares
it applies to the given workspace dir via a new field:
# open_source/workspace_mcp.yaml
workspace:
kind: github
applies_to: ./repos # opt-in: parent-walk auto-detection allowed
# for `--workspace open_source/repos/`
When the operator passes --workspace open_source/repos/:
Primary location (
open_source/repos/workspace_mcp.yaml) is checked first; if present, it wins unconditionally.If absent, the parent (
open_source/workspace_mcp.yaml) is considered — but only if it declaresworkspace.applies_to: ./reposAND that path canonicalises to the actualworkspace_dir.
Without the declaration (or with a mismatched path), the framework
refuses to auto-detect — operator must pass --mcp-config explicitly.
The opt-in eliminates the accidental-discovery footgun where a
project-root manifest would silently attach to any sibling dir an
operator happened to pass to --workspace.
Why opt-in¶
The original design considered an unconditional parent-walk fallback.
That would have introduced a regression: an operator with
~/proj/workspace_mcp.yaml (for their real workspace) accidentally
running mcp-server --workspace ~/proj/some_other_dir/ would silently
inherit the wrong manifest. The opt-in eliminates this entirely — the
manifest author declares which child the manifest covers, and the
framework only matches that declaration.
The natural layout this enables:
open_source/
├── workspace_mcp.yaml # declares workspace.applies_to: ./repos
└── repos/ # --workspace points here; auto-detect works
Operators in this layout no longer pass --mcp-config for every
workspace launch.
New schema field¶
workspace.applies_to: <relative path> — optional string, must be
non-empty when set. Resolved against the manifest’s parent
directory. Unset = no parent-walk discovery for this manifest (the
safe default for existing manifests).
Manifest::to_json() emits the new field under workspace.applies_to
(string or null). Non-breaking JSON shape addition under the existing
stability guarantee.
Logging¶
When the parent-walk fallback is considered, the framework emits a
tracing log at INFO level explaining the discovery outcome:
"manifest discovered via parent-walk fallback (workspace.applies_to matched)"— on success"parent-walk manifest does not declare workspace.applies_to; ignoring"— declaration absent"parent-walk manifest's workspace.applies_to does not match this workspace_dir; ignoring"— mismatch
Operators with RUST_LOG=info (or any tracing subscriber wired up by
their binary, which mcp-server’s init_tracing does by default)
see exactly why discovery did or did not succeed.
Tests¶
Six tests under find_workspace_*:
find_workspace_works— primary location (existing).find_workspace_walks_one_level_up_with_applies_to— opt-in match returns the parent manifest.find_workspace_ignores_parent_without_applies_to— parent manifest without declaration is NOT auto-detected.find_workspace_ignores_parent_with_mismatched_applies_to— declaration that doesn’t match is rejected.find_workspace_returns_none_when_missing_everywhere— no manifest in child or parent → None.find_workspace_primary_wins_over_parent_fallback— primary always preempts even when both could match.
111 unit tests pass (was 108 at 0.3.31 head; +3 net after refactor — two new opt-in tests, the existing walks-up test renamed to make the opt-in requirement explicit).
Origin¶
kglite operator audit during the 0.6.x → 0.9.x migration: workspace
manifests sitting next to (not inside) the workspace dir required
--mcp-config for every launch. They proposed and implemented the
parent-walk in their local tree; we picked the opt-in shape over
their unconditional version to eliminate the accidental-discovery
risk.
0.3.31 — 2026-05-13¶
Added — tools[].bundled: override shape¶
Manifest tools: arrays now accept a third entry kind alongside
cypher: and python:: bundled: entries customise the
agent-facing surface of a bundled tool the downstream binary
provides natively (e.g. cypher_query, repo_management,
graph_overview).
tools:
- bundled: repo_management # name in the value, no `name:` line
description: | # override the agent-visible description
FIRST STEP for this server. Call repo_management('org/repo')
to clone + build a repo before any other tool.
- bundled: ping # hide a tool from tools/list AND
hidden: true # reject calls to it
- name: similar_sessions # existing cypher-tool shape unchanged
cypher: |
MATCH ...
Field semantics:
Field |
Type |
Default |
Notes |
|---|---|---|---|
|
string |
(required) |
The bundled tool’s name. Must match |
|
string |
None |
Replaces the bundled tool’s default agent-facing description. |
|
bool |
false |
When true, the downstream consumer should omit the tool from |
Mutually exclusive with cypher: / python: / name: /
parameters: / function:. The parser surfaces clear errors at
boot for combinations that don’t fit.
Motivation¶
The pre-0.3.31 customisation surface for bundled tools was the
manifest’s global instructions: block — useful for first-message
agent orientation, but a single blob detached from individual
tools. Operators wanting to teach agents that repo_management is
the FIRST STEP, or hide ping from a production server, had to
stuff that into the global instructions and hope the agent
correlates it. Bundled overrides attach those concerns to the
tools directly: descriptions ride the tools/list response next
to the schemas, and hidden:true is the surgical opt-out for
“this tool is in the bundled catalogue but my agent shouldn’t see
it.”
The downstream kglite-mcp-server is the first consumer; it ships
bundled-override support in its next release after this lands. The
framework half of the work is small (~150 LoC parser + struct +
to_json variant + 11 tests) and entirely additive — pre-existing
tools[].cypher and tools[].python entries parse unchanged.
JSON shape addition¶
Manifest::to_json() emits bundled override entries as:
{
"kind": "bundled",
"name": "repo_management",
"description": "FIRST STEP for this server...",
"hidden": false
}
Non-breaking field addition to the existing tools[] array shape.
Consumers that ignore unknown kind values stay safe; consumers
that want bundled-override support match on kind == "bundled".
Tests¶
11 new parser tests in crates/mcp-methods/src/server/manifest.rs::tests:
bundled_override_with_description_parsesbundled_override_with_hidden_parsesbundled_override_alongside_cypher_tools_parsesrejects_bundled_with_cypher_kindrejects_bundled_with_name_fieldrejects_bundled_with_parameters_fieldrejects_bundled_with_non_bool_hiddenrejects_hidden_on_cypher_toolrejects_duplicate_bundled_overridesrejects_bundled_with_invalid_identifierbundled_override_to_json_shape
Plus the existing 93 mcp-methods tests stay green (additive change; no behaviour shift on the cypher / python paths).
Fixed — repo_management cross-binary surface drift¶
The generic mcp-server CLI used to always register repo_management
in tools/list, regardless of whether a workspace was bound. Without
a workspace, calls to the tool returned "repo_management requires --workspace mode." — but the tool was still visible to the agent.
Downstream binaries (e.g. kglite-mcp-server) gate the same tool
out of tools/list entirely when no workspace is bound. The
inconsistency surfaced when operators ran the same YAML against
mcp-server (saw the tool) and kglite-mcp-server (didn’t see it).
McpServer::new now calls tool_router.remove_route("repo_management")
when options.workspace.is_none(). Tool surface matches downstream
gating; agents only see the tool when it can do something useful.
Two new tests anchor the behaviour:
repo_management_gated_to_workspace_mode— bare framework, no workspace, tool is absent fromlist_all().repo_management_present_when_workspace_bound— with a workspace, tool is present.
Reported by kglite while comparing the two binaries against the same YAML post-0.9.25 deployment.
0.3.30 — 2026-05-12¶
Distribution shape — bundled binary in the pip wheel¶
pip install mcp-methods now puts the mcp-server CLI on PATH again,
restoring the pattern we shipped in 0.3.25. The wheel bundles the
native Rust binary at mcp_methods/_bin/mcp-server; the Python entry
point mcp_methods._cli:main execs it on invocation.
This is the kglite-style “pip install gives you everything” UX,
adapted for our case: because our mcp-server binary has zero
libpython link (kept from the 0.3.26 pure-Rust separation), we stay
at 3 abi3 wheels per OS — no per-Python-version matrix.
Why the revert¶
The 0.3.26 polars-shape split removed the bundled binary on
architectural grounds. Re-introducing it: pip is the operator-facing
distribution and the CLI is the operator-facing artefact. A separate
crates.io entry for mcp-server (the 0.3.29 plan) would have added a
distribution path nobody asked for. Single distribution path is
clearer.
crates/mcp-server/ stays in the workspace as the binary’s source
home — used by the wheel build to compile + bundle, and available
for downstream Rust users via cargo install --git. Not published
to crates.io.
crates.io — library publication¶
mcp-methods 0.3.30 is the first version published to crates.io.
The library is pure Rust, zero pyo3 in the dep tree (the polars
shape from 0.3.26 is preserved). docs.rs auto-builds the rustdoc.
Downstream consumers can now write:
mcp-methods = "0.3"
…instead of pinning to a git rev. kglite’s existing git-rev pin keeps working unchanged.
Files touched¶
pyproject.toml— restored[project.scripts]+[tool.maturin] includeblocks.python/mcp_methods/_cli.py— restored.Makefile— restoredbundle-bin/dev-with-bintargets..github/workflows/build_wheels.yml— restored the cargo-build-binary + copy-into-wheel step (per-OS)..gitignore— re-ignorepython/mcp_methods/_bin/.crates/mcp-methods/Cargo.toml— publish metadata (repository, keywords, categories, docs.rs config).crates/mcp-server/Cargo.toml— version 0.3.29 → 0.3.30 (still workspace-local; not published).README.md — drop the
cargo install mcp-serversection; fold the CLI into the Python-install path.
No regression for downstream Rust consumers¶
The library crate source is unchanged. kglite’s rev = "71f7ba6..."
pin resolves identically. The new 0.3.30 publish to crates.io is
opt-in — downstream consumers who want to switch from git-rev to
version = "0.3" can, but nothing forces them.
0.3.29 — 2026-05-12¶
Added¶
trust.allow_query_preprocessor— third advisory trust gate alongsideallow_python_toolsandallow_embedder. The framework parses the bool and surfaces it throughTrustConfigandManifest::to_json(); downstream consumers enforce. Mirrors the existingallow_embedderpattern: the framework records the operator’s declared trust, the consumer (e.g.kglite-mcp-server) refuses to boot the corresponding extension hook when the flag is false. Driven by the mcp-servers-operator’s 0.9.25 Cypher preprocessor spec.
JSON shape¶
Manifest::to_json()now emitstrust.allow_query_preprocessor. Non-breaking field addition; consumers that ignore unknown keys are unaffected. Theto_json_shape_is_stablesnapshot test has been updated to expect the new key.
Tests¶
allow_query_preprocessor_trust_parses— positive case.allow_query_preprocessor_rejects_non_bool— type validation.
0.3.28 — 2026-05-12¶
Fixed¶
Workspace::set_root_dirno longer clobbers the just-setactive_repo_pathback to the configuredworkspace_dir. The local-mode branch ofclone_or_updatenow reads the currentactive_repo_pathfrom state (falling back toworkspace_dironly when state is unset), so the activate-after-set_root_dir sequence preserves the new root, and the post-activate hook fires against the bound target rather than the original configured root.Reported by kglite while wrapping
Workspacein pyo3 for 0.9.24. Bug had no effect on github-mode workspaces; only local-modeset_root_dircallers who readactive_repo_path()after the swap (or whose post-activate hook needed to rebuild against the new root) were affected.
Added (tests)¶
set_root_dir_updates_active_path— anchors the invariant.set_root_dir_post_activate_fires_against_new_root— confirms the hook fires against the bound target.
0.3.27 — 2026-05-12¶
Added¶
Manifest::to_json() -> serde_json::Valuefor FFI / RPC introspection. Returns a JSON-friendly view of the validated manifest with stable shape across patch releases. Intended for pyo3 wrappers, JSON-RPC bridges, and any consumer that needs programmatic access to the loaded manifest without per-field getters. Schema knowledge lives end-to-end on the framework side; downstream wrappers shrink to roughly one passthrough method plus a genericserde_json::Value -> PyObject(or equivalent) converter.
Notes¶
No breaking changes. No new dependencies (
serde_jsonwas already in the dep tree).The JSON shape is pinned by the
to_json_shape_is_stabletest incrates/mcp-methods/src/server/manifest.rs— read it as the canonical contract. Future renames or removals in this shape are breaking changes; additions are non-breaking.No
#[derive(Serialize)]on any manifest struct: keeps the hand-rolled parsing symmetry on input and output, and avoids coupling JSON shape to Rust field names.
0.3.26¶
The polars / pydantic-core distribution shape. mcp-methods is now a
pure-Rust library with zero PyO3 in source or deps; the Python wheel
is built from a separate mcp-methods-py binding crate. cargo add mcp-methods from Rust sees no traces of Python anywhere.
Workspace restructure¶
Three-crate workspace (one was four-crate-ish in 0.3.25 with bundled- binary machinery; that’s gone too):
crates/mcp-methods— pure-Rust library (rlib only). Contains every primitive (cache,compact,files,git_refs,github,grep,html,json_grep,list_dir) plus theserverframework module behind the default-onserverfeature. Nopyo3, no#[pyfunction], noPy<…>, noPyResultanywhere in the source. CI verifies viacargo tree -p mcp-methods | grep pyo3as a fail-on-leak gate.crates/mcp-methods-py— PyO3 bindings crate (cdylib only). Depends onmcp-methodsand provides#[pyfunction]/#[pyclass]wrappers around the library functions.PyElementCacheis a newtype wrapper overmcp_methods::cache::ElementCache. The wheel is built from this crate viapyproject.toml’smanifest-pathsetting.crates/mcp-server— standalone CLI binary. Depends onmcp-methodswithfeatures = ["server"]. Published to crates.io alongside the library; install viacargo install mcp-server.
Distribution¶
Audience |
Command |
Result |
|---|---|---|
Rust library users |
|
Pure-Rust crate. No pyo3 in dep tree. |
Python users |
|
One abi3 wheel per OS (3 total) — works on Python 3.10 through 3.13 without reinstall. Down from 12 per-Python-version wheels in 0.3.25. |
CLI users |
|
Binary on PATH. No libpython link. |
The bundled-binary mechanism from 0.3.25 is gone: no more
mcp_methods/_bin/, no _cli.py Python launcher, no
[project.scripts] entry, no maturin include for the binary, no
make bundle-bin. pip install mcp-methods ships only the cdylib
and Python wrapper; the binary lives in its own crate.
Python embedder: / tools[].python: removed¶
The legacy framework hooks for loading Python embedder classes and
Python tool callables were removed. They required PyO3 in the
framework’s source — incompatible with the pure-Rust contract. No
deployed manifest uses tools[].python:, and the lone embedder
consumer (kglite’s sodir / norwegian_law / petrel servers) migrated
to fastembed-rs in 0.9.18. Downstream binaries that need Python
extension hooks add a pyo3 wrapper layer in their own cdylib.
API shape changes¶
Callback-bound functions kept their Python-side surface (the wheel’s
read_file(transform=…), list_dir(annotate=…), ripgrep_files(transform=…)
still take callables). On the Rust side, the callback is now a
generic &dyn Fn(...) parameter on a ReadFileOpts / ListDirOpts
/ RipgrepFilesOpts struct. Pure-Rust callers default-construct
the opts and skip the callback; the binding crate bridges
Py<PyAny> → Rust closure that re-enters Python via Python::attach.
compact_discussion returns Result<(String, Option<String>), String>
in Rust; the binding crate’s wrapper maps to PyValueError.
ripgrep_lines and ripgrep_json_fields return typed
Vec<RipgrepLinesGroup> / Vec<JsonGrepMatch>; the wrapper converts
to Python lists of dicts.
Migration¶
For Python users: zero changes. from mcp_methods import …
imports identical. Wheel layout (mcp_methods/_mcp_methods.so)
preserved.
For Rust library consumers (kglite, etc.): Cargo.toml stays the
same — mcp-methods = { …, features = ["server"] } still works
because we kept the server feature default-on. Source paths are
unchanged: mcp_methods::server::McpServer, mcp_methods::cache::ElementCache,
mcp_methods::github::fetch_issue_internal, etc. — all in the same
locations. Two things to know about:
mcp_methods::server::embedderandmcp_methods::server::pythonmodules are gone. kglite stopped using them in 0.9.18; if a future consumer needed them, they re-implement in their own pyo3 crate.mcp_methods::server::apply_python_extensionsandPythonExtensionsare gone for the same reason.
For CLI users: the binary distribution changed. Old: pip install mcp-methods put it on PATH. New: cargo install mcp-server.
Reflects the binary’s actual nature (a Rust CLI tool, not a
Python package).
Tests¶
87 cargo unit tests + 7 mcp-methods integration tests + 7 mcp-server
binary tests + 176 python tests = 277 total. cargo tree -p mcp-methods | grep pyo3 is a CI gate; if it ever returns non-zero, the build fails.
0.3.25¶
Structural consolidation + the long-tail of the PyO3 decoupling work.
This release closes both distribution goals: pip install mcp-methods
puts mcp-server on PATH, and downstream Rust binaries
(kglite-mcp-server, etc.) can build with no transitive libpython
linkage via default-features = false.
Workspace consolidation¶
crates/mcp-serveris gone. Its modules moved into the rootmcp-methodscrate under a newserverfeature. Every public type and function name is preserved — paths shift by one segment:mcp_server::McpServer→mcp_methods::server::McpServermcp_server::manifest::ToolSpec→mcp_methods::server::manifest::ToolSpecmcp_server::build_tool_attr→mcp_methods::server::build_tool_attretc.
Lib name renamed
_mcp_methods→mcp_methods. The leading- underscore convention is Python-internal; in Rust it forced consumers to writeuse _mcp_methods::*. Now they writeuse mcp_methods::*. Python’sfrom mcp_methods._mcp_methods import …still works — maturin’smodule-nameoverride places the cdylib at the same wheel path as before.crate::cache::ElementCache,crate::github::*,crate::git_refs::*,crate::compact::*,crate::html::*stay at the crate root — intra-crate references shift from_mcp_methods::*tocrate::*.
pip install mcp-methods ships mcp-server on PATH¶
Wheel now bundles the native Rust binary under
mcp_methods/_bin/mcp-serverand registers a Python console-script entry point (mcp-server = "mcp_methods._cli:main") that execs it.make dev-with-binbuilds + bundles the binary locally; the wheel- build CI workflow runs the same sequence before maturin packages.Per-platform wheel size grows ~10 MB (the native binary).
Cargo features¶
The crate’s feature surface is now coherent across both build modes:
default = ["python", "server"]— standardcargo installor wheel build path includes everything.python— opt-out drops PyO3 (no_mcp_methodscdylib, no callback-bound helpers likeread_file/ripgrep/list_dir/json_grep, no manifest-declaredpython:tools orembedder:blocks).server— opt-out drops rmcp + tokio + clap + the framework modules + themcp-serverbinary.python-extension— impliespython, addspyo3/extension-modulefor the wheel cdylib.
A --no-default-features build is the pure-Rust primitives subset.
A --no-default-features --features server build is the
framework-only path with no libpython linkage — verified with
otool -L target/debug/mcp-server showing no libpython3.x.dylib.
Downstream migration (kglite + similar)¶
# Before — two separate deps:
mcp-methods = { git = "...", rev = "0.3.24", default-features = false }
mcp-server = { git = "...", rev = "0.3.24", default-features = false }
# After — one dep with the `server` feature:
mcp-methods = { git = "...", rev = "0.3.25", default-features = false, features = ["server"] }
Source rename pattern: mcp_server::X → mcp_methods::server::X;
_mcp_methods::X → mcp_methods::X.
CI¶
Workspace-root rows added:
cargo build/test,cargo build/test --no-default-features,cargo build/test --no-default-features --features server.Wheel-build workflow updated to build + copy the bundled binary before maturin runs.
Tests¶
90 cargo unit tests (was 17 + 73 split across two crates) + 7 manifest regression + 7 binary CLI tests + 19 smoke + 157 python = 280 tests pass.
All 5 deployed YAML manifests parse unchanged.
0.3.24¶
Responds to three kglite asks (inbox/read/2026-05-11-*). Three phases land together; consumer-visible APIs remain backward-compatible.
mcp-server framework¶
extensions:top-level manifest passthrough. New top-level YAML key that the framework accepts but does not validate. Stored onManifestasextensions: serde_json::Map<String, serde_json::Value>. Downstream binaries read whatever keys they need from it. Strict-unknown-key validation stays in force for the framework’s own surfaces (builtins:,workspace:, …) — only theextensions:block is intentionally unvalidated. Closes kglite’s csv-http extension ask; matches the domain-agnostic boundary set in 0.3.23.PyO3 is now an optional Cargo feature on
crates/mcp-server. Newpythonfeature incrates/mcp-server/Cargo.toml, default-on. Standalonecargo install mcp-serveris unchanged. Downstream binaries can disable withdefault-features = falseto remove the direct PyO3 dep from this crate. Note: the top-levelmcp-methodscrate (the Python wheel) still depends on PyO3 unconditionally — that is by design. The feature flag onmcp-serverremoves the direct link from this crate only; for a fully PyO3-free downstream binary the consumer also needs to gate themcp-methodsdep itself. CI now runs bothcargo build -p mcp-server --no-default-featuresandcargo test -p mcp-server --no-default-featuresto catch leaks.Cfg-gated modules:
python.rsandembedder.rsare now#[cfg(feature = "python")].runtime::PythonExtensionsandruntime::apply_python_extensionsare similarly gated.mcp-serverbinary’smain()emits awarn-level log if a manifest declarespython:tools orembedder:in a binary built without thepythonfeature, so operators aren’t silently ignored.
Tests + infrastructure¶
Framework smoke-test suite. New
tests/test_mcp_server_smoke.pydrives themcp-serverbinary over JSON-RPC stdio and exercises every tool category: source navigation, GitHub access (with and without token),.envwalk-up + explicitenv_file:, local- workspace mode, bare boot. 19 tests, ~3 seconds, auto-skips when the binary isn’t built. Adapted from kglite’s smoke suite shape; kglite-specific tests (graph / Cypher / read_code_source) stay in their repo.
0.3.23¶
Patch release in response to kglite’s 0.3.22 smoke-test findings (inbox/read/2026-05-10-from-kglite-smoke-test-findings.md). The framework stays domain-agnostic — Cypher tool registration is a graph-engine concern, so it stays on the kglite side. What we expose here is the primitive that lets downstream binaries build that registration cleanly.
mcp-server framework¶
mcp_server::build_tool_attrnow public. Closes the primitives half of Gap A: downstream binaries can now build the rmcpToolattr from(name, description, json_schema)in the same shape the framework uses internally and pair it withToolRoute::new_dynto register their own dynamic tools — Cypher runners, custom DSL handlers, anything else. No API divergence between the framework’s own registrations and downstream ones.
mcp-methods (Rust core)¶
auth_tokenfilters empty strings. Closes Gap C: an env var set to""(e.g. for clearing the token without unbinding) was reported as “token present” byhas_git_token(), causing the github tools to register and then 401 on the first call. Now an empty string is treated identically to a missing var. Test added.
0.3.22¶
Closing the kglite wishlist gaps (inbox/read/2026-05-10) so the 5 deployed YAML manifests + ~3,000 LoC of custom Python MCP servers can retire onto this framework.
mcp-server framework¶
.envauto-loading at startup. Walks upward from the workspace / source-root / watch / cwd looking for.env; loadsKEY=VALUElines into the process env (skip blanks/#, strip outer quotes, do not overwrite existing). Operators who want a non-implicit pick declareenv_file: ../.envat the YAML top level.github_issueselement drill-down. Newelement_id/lines/grep/context/refresharguments on the MCP tool. FETCH responses cache collapsedcb_N/patch_N/comment_N/overflowelements server-side (via_mcp_methods::cache::ElementCache); passelement_id="cb_1"(with the samenumber=N) to retrieve a single element without re-fetching, optionally narrowed bylines="40-60"orgrep="pat".Honest tool listing.
github_issuesandgithub_apiare now registered dynamically at boot only whenGITHUB_TOKENis set; they no longer appear in tool-listing responses when the agent couldn’t use them. Boot-time decision — restart to pick up a token that appears later. The framework binary logs aninfoline announcing the skip.Workspace::last_built_sha(name). New public reader exposing the HEAD SHA persisted after the last successful post-activate hook for a repo. Backed by an additivelast_built_shafield on the per-repoinventory.jsonentry (#[serde(default)]keeps older inventories loading cleanly). Foundation for the auto-rebuild-gating work landing in Phase C.Embedder lifecycle (
mcp_server::embedder). New module exposingEmbedderHandle(load/unload/embed/touch + idle tracking) andspawn_idle_watchfor the eviction tokio task.PythonExtensionsnow yieldsOption<Arc<EmbedderHandle>>plusembedder_cooldownandembedder_watcher. The framework owns the cooldown timer; the value is extracted fromembedder.kwargs.cooldownin the manifest. Embedder classes must exposeembed+dimension;load/unloadare optional but called automatically when present.Manifest schema — new
env_file:top-level key (added toALLOWED_TOP_KEYS; strict-unknown-key validation preserved).Auto-rebuild gating on
repo_management(update=True). Captures the new HEAD SHA on each clone/fetch; when the gate seesaction == "current"ANDprev_built_sha == new_headAND the user did not passforce_rebuild, the post-activate hook is skipped and the response carries a[build skipped: ...]marker. Newforce_rebuild: boolargument on therepo_managementMCP tool bypasses the gate (useful after the builder itself has been upgraded).workspace.kind: localmode — new top-level manifest blockworkspace: { kind, root, watch }.kind: localbinds a fixed directory as the source root and reuses the same auto-rebuild gating (with a cheap recursive-mtime fingerprint instead of git HEAD SHA). When the manifest declareskind: local, that wins over the CLI--workspaceflag’s interpretation — manifest is the source of truth, same rule assource_root:.watch: truewires the framework’s debounced file watcher to the root for hot-reload rebuilds. Local mode rejectsname=/delete=trueinrepo_managementwith a friendly error. Newset_root_dir(path)MCP tool (registered automatically when in local mode) swaps the active root at runtime — drop-in for the legacycode_reviewserver workflow.inventory.jsonis stored under<root>/.mcp-workspace/in local mode to keep the user’s tree clean.McpServer::builtins()public getter. Surfaces the manifest’sbuiltins:block (save_graph,temp_cleanup) verbatim so downstream consumers (e.g. kglite’sgraph_overviewtool deciding whether to wipetemp/) can read the operator’s intent without re-parsing YAML. The framework does not act on these — it doesn’t know what an “overview” call means.
mcp_methods.fastmcp (Python)¶
New composable helpers for FastMCP authors so each one can drop the
boilerplate that the YAML+CLI binary handles. Each helper takes the
FastMCP app and the dependency it needs (graph, source roots, …)
and registers the same tools the bundled binary ships, one-to-one.
register_overview(app, graph, overview_prefix=None)—graph_overview.register_cypher_query(app, graph, csv_dir="temp/")—cypher_querywithformat="csv"writing uuid-named files for streaming/exports.register_source_tools(app, source_roots=[...])—read_source,grep,list_sourcewith the same path-sandbox semantics as the Rust framework (os.path.realpathrejects traversal). Wraps the existing PyO3 surface; ~10-line wrappers, no logic duplication.register_save_graph(app, graph)—save_graph(path).serve_csv_via_http(directory, port=0, bind="127.0.0.1")— CORS- enabled HTTP server for serving CSV exports to browser-side agent contexts. Returns(server, base_url); runs in a daemon thread.
A runnable end-to-end stub lives at examples/fastmcp_demo.py.
Documentation + regression suite¶
README “Deployment” section. Documents the
cargo install --path crates/mcp-serverrecipe, the symlink path for deployments that pinned a binary elsewhere, and a table of the operating modes (bare / source-root / workspace[github] / workspace[local] / watch) with how to set each via CLI flag or manifest.Regression test for deployed manifests. New integration test (
crates/mcp-server/tests/deployed_manifests.rs) asserts the schema shapes used by the 5 production manifests at/Volumes/EksternalHome/Koding/MCP servers/continue to load cleanly. If any starts failing after a schema change, a production manifest has been broken — fix the schema or migrate the deployment.
0.3.21¶
McpServer::register_typed_tool<T, F>(name, description, handler)— typed dynamic-tool registration helper. Compresses the boilerplate of building aToolattr from a JSON Schema (via schemars), serde- deserialising per-call arguments, and wrapping the handler in aToolRoute::new_dynclosure. Domain binaries (kglite-mcp-server) use this to register their tools in ~5 lines instead of ~35. The handler isFn(T) -> String; state lives in the closure environment.
0.3.20¶
Added — mcp-server crate (Rust-native MCP server framework + binary)¶
A new sibling crate at crates/mcp-server/ providing a Rust-native MCP
server built on the official rmcp SDK (v1.6) with a stdio transport.
Designed to replace the Python kglite.mcp_server over the next few
phases. The new binary is mcp-server.
CLI refit — drop --graph (graph concept lives in downstream
binaries like kglite-mcp-server, not in mcp-methods). Replace with
--source-root DIR for direct binding of the source tools to a fixed
directory. Modes are now: bare / --source-root / --workspace /
--watch. Help text rewritten to clarify the framework’s domain-
agnostic role; downstream binaries (kglite, etc.) layer their domain
tools on top by re-using McpServer::new with custom registrations.
Also dropped --embedder (the manifest’s embedder: block remains
the source of truth).
Library extraction (post-phase-6). The crate now exposes a
[lib] target alongside the [[bin]], with the framework boilerplate
(apply_python_extensions, resolve_source_roots, init_tracing,
maybe_watch) lifted into a new mcp_server::runtime module. This
lets downstream binaries (kglite-mcp-server, etc.) reuse the entire
boot sequence without copy-pasting hundreds of LoC of glue.
mcp_server::python::json_to_py is now pub so dynamic-tool
implementations can reuse it for forwarding JSON kwargs to Python
callables (e.g. graph.describe(**kwargs)).
Phase 6 — Workspace mode (--workspace DIR). Multi-repo
clone-and-track flow with idle-sweep inventory.
repo_managementMCP tool: passname='org/repo'to clone (if missing) and activate;delete=trueto remove;update=trueto fast-forward the active repo; no args to list with access counts.Active repo state lives in
Workspace::Arc<RwLock<…>>; source tools pick up the swap on the very next call (with_workspacewires dynamic source-roots and default-repo providers).Inventory persists to
<workspace>/inventory.jsonwithcloned_at/last_accessed/access_count/staleper repo; reconciles with on-disk state at boot (un-tracked clones get inventory entries; vanished repos marked stale).Auto-sweeps idle repos older than
--stale-after-days(default 7); active repo is exempt; stale entries preserve their access history even after deletion.Git operations shell out to
git(clone –depth 1, fetch + reset –hard FETCH_HEAD); nolibgit2dep.PostActivateHookcallback type lets downstream binaries fire custom logic after each successful clone/update — kglite-mcp-server will use this to invokecode_tree::buildon the freshly-activated repo and pin the resulting graph.Self-contained ISO-8601 (seconds-precision) formatter avoids pulling in
chronofor a handful of timestamps.End-to-end test: cloned
rust-lang/rustlingsfrom github.com,list_sourcereturned the active repo’s tree,repo_management()listing shows it as[active]with access counts.
Phase 5 — Watch mode (--watch DIR). The CLI now spawns a
recursive debounced filesystem watcher (default 500 ms debounce) that
logs change events at INFO level and can fire a downstream-supplied
callback on each batch. Source roots are auto-pinned to the watched
directory so the source tools (read_source / grep / list_source)
operate on the live tree. Powered by the notify + notify-debouncer-mini
crates. Downstream binaries (kglite-mcp-server, etc.) plug in their
rebuild logic via the ChangeHandler callback type — phase-6 work
will wire one in for code-tree rebuilding once kglite layers in.
Phase 4 — Python extension layer. Embeds CPython into the
binary via PyO3’s auto-initialize feature so manifest-declared
python: tools and custom embedder factories work out of the box.
Manifest
tools: [{ name, python: ./X.py, function: F }]entries load viaimportlib.util(nosys.pathmutation), introspect the function signature withinspect.signatureto derive a JSON Schema for MCP, and register dynamically on rmcp’s tool router viaToolRoute::new_dynso the agent sees them intools/listalongside the built-in source / GitHub tools.Manifest
embedder: { module, class, kwargs }block is loadedinstantiated at boot. The PyObject is held in memory; downstream binaries (kglite-mcp-server) wire it to a graph’s text-score path via their own integration.
Two-signal trust gating:
python:tools require bothtrust.allow_python_tools: trueand--trust-tools; embedders requiretrust.allow_embedder: true+--trust-tools. Refusing either way is the default. Boot-time errors are clear and specific.Type-hint → JSON Schema mapping:
str → string,int → integer,float → number,bool → boolean,list → array,dict → object. Unmatched annotations fall back tostringwith the Python repr in the schema’stitlefield. Defaults are JSON-encoded intodefault.ServerHandlerimpl onMcpServerswitched fromSelf::tool_router()(static) toself.tool_router(instance) so dynamic routes added before serving show up intools/list.End-to-end test: a
def greet(name: str, count: int = 3) -> strPython function loads, registers with the right schema (namerequired,countdefaulted), and dispatches correctly throughtools/call.
The FastMCP-API-compat shim (from mcp_methods.fastmcp import FastMCP)
is deferred to a follow-up. The manifest form already accepts any
plain Python function — users with existing FastMCP code just declare
each decorated function as a separate manifest entry until the shim
ships.
Phase 3 — GitHub tools (github_issues, github_api) registered
on the rmcp server. Both tools resolve the active repo via a
caller-supplied dynamic provider with an optional per-call repo_name=
override; auto-detects from cwd’s git remote as a last resort.
github_issues covers all three modes — FETCH (number=), SEARCH
(query=), LIST (no args) — by delegating to the existing
mcp_methods::github Rust internals (no PyO3 in the hot path). To make
that delegation possible, mcp-methods now ships as both a Python
extension (cdylib) and a regular Rust library (rlib), with the
extension-module PyO3 feature gated behind the new python-extension
Cargo feature so cargo build can produce both. pyproject.toml
maturin features updated accordingly. End-to-end stdio test against
github.com/rust-lang/rustlings confirmed.
Phase 2 — source tools (read_source, grep, list_source)
registered on the rmcp server. Each tool is gated on the server having
an active source-roots provider (static or dynamic); when none is
configured, the tool returns a friendly “configure source_root in your
manifest” error rather than crashing. Implementation lives in
crates/mcp-server/src/source.rs and uses the same ignore +
grep-matcher/grep-regex/grep-searcher crates as the existing
mcp-methods primitives. ServerOptions gains with_static_source_roots
with_dynamic_source_roots. Manifestsource_root(s)are now canonicalised at boot and wired into the server. 15 new unit tests covering read/grep/list semantics, glob filtering, traversal blocking.
Phase 1 — bootstrap. Boots a working MCP server with the framework wired end-to-end, plus the manifest schema parsed and validated. No real tools yet — that’s phase 2+.
Cargo workspace conversion of
mcp-methods(root crate unchanged;crates/mcp-server/added as a sibling member).YAML manifest parser at
crates/mcp-server/src/manifest.rs— direct port of the Pythonkglite.mcp_server.manifestschema. Same keys (name,instructions,overview_prefix,source_root(s),trust.{allow_python_tools, allow_embedder},tools,embedder,builtins.{save_graph, temp_cleanup}), same validation messages, same auto-detection of sibling (<basename>_mcp.yaml) and workspace-level (workspace_mcp.yaml) manifests.clap-based CLI matching the Python flags:--graph/--workspace/--watch(mutually exclusive),--mcp-config,--embedder,--name,--trust-tools,--stale-after-days.rmcp
ServerHandlerimpl with onepingtool to verify the framework dispatch is wired. End-to-end stdio handshake confirmed (initialize → tools/list → tools/call).25 unit tests covering manifest parsing edge cases and CLI mode picking.
0.3.19¶
Built-in
html_to_textfunction — converts HTML to clean, readable text optimized for LLM consumption. Strips<head>,<script>,<style>, and comments. Converts headings to markdown#prefixes, list items to-bullets, bold/strong to**text**, images to[image: alt], tables to tab-separated text, and links to plain text. Decodes 75+ named HTML entities (including Scandinavian æøå) plus numeric/hex references. Available as a standalone function (from mcp_methods import html_to_text) and as a string-based transform onread_file(transform="html").String-based
transformonread_file—transformnow accepts"html"in addition to callables. The built-in html transform runs after section extraction (soidattributes remain available) but before grep (so patterns match clean text, not raw tags). Callable transforms preserve existing behaviour.
0.3.17¶
Neutral error for non-existent items — fetching a number that doesn’t exist as an Issue, PR, or Discussion now returns
#N not found in repo (checked Issues, PRs, and Discussions)instead of leaking the GraphQL fallback error ("Could not resolve to a Discussion").
0.3.16¶
Renamed
github_discussions→github_issues— one tool, three modes: FETCH (by number), SEARCH (by query), LIST (default).github_discussionsremains as a backward-compat alias.Search mode —
query="datatree coordinates"searches via RESTsearch/issuesfor issues+PRs, and GraphQLsearch(type: DISCUSSION)for Discussions.kindroutes to the right API;kind="all"runs both and concatenates results. Sort defaults to relevance for search,"created"for listing.Renamed
ElementCache.fetch_discussion→fetch_issue.labelsparameter simplified — now a comma-separated string ("bug,P0") instead ofVec<String>.sortparameter now optional — defaults depend on mode:None(relevance) for search,"created"for listing.
0.3.15¶
GitHub Discussions support (GraphQL) —
fetch_discussionandgithub_discussionsnow transparently handle GitHub Discussions, not just Issues and PRs. When a REST lookup returns 404, the library falls back to the GraphQL API to fetch the Discussion with full threaded comments (top-level + nested replies), category, and answered status. Listing mode supportskind="discussion"to query Discussions via GraphQL with state and sort filtering. Ref collection also extracts GitHub references from threaded reply bodies.
0.3.14¶
max_matchesparameter forread_filegrep — limit the number of matches returned when grepping within a file. When a dense document has 125 hits,max_matches=20returns the first 20 with context, and the header changes to “showing 20 of 125 matches”. The ripgrep module already hadmax_results/match_limit; this brings the same control to single-file grep. Also improved truncation footers: whenmax_charscuts grep output, the footer now includes the total match count (e.g., “125 matches, 279865 chars total”) so you know whether to refine the pattern or increase the limit.
0.3.13¶
grepparameter forread_file— search within a single file and return only matching lines with context. Avoids reading entire large documents into context when only specific passages are needed. Supportsgrep_context(default 2) for surrounding lines, merges overlapping context windows, works withsection,start_line/end_line,transform, andmax_chars. Uses--separators between non-contiguous match groups, consistent with ripgrep output.
0.3.12¶
UTF-8 fix for
sectionextraction — the byte-stepping loop panicked on multi-byte characters (§, æ, ø, å, é, etc.) inside extracted sections. Now advances by full UTF-8 codepoints. Also hardened allmax_charstruncation sites to avoid splitting multi-byte characters.
0.3.11¶
sectionparameter forread_file— extract an HTML element by itsidattribute. Solves single-line HTML navigation: instead of getting 127KB on line 96, requestsection="PARAGRAF_4-7"to get just that element. Infers tag name from the matched opening tag, handles nested elements of the same type, works withtransformandmax_chars.
0.3.10¶
Comment TOC — bare
element_id="comments_middle"returns a lightweight table of contents (_index,author,created_at, 80-char snippet) instead of dumping raw JSON.
0.3.9¶
lineson array elements —lines="1-20"oncomments_middlereturns structured comment objects by index range._indexon highlights — maintainer highlights include_indexfor drill-down.Grep metadata on comments — grep matches include
author,created_at,comment_index, andelement_id.
0.3.7¶
Thread digest — discussions with 50+ comments are condensed: first 5 + maintainer highlights + last 5 inline, middle cached as
comment_Nand searchablecomments_middle.Bookend pagination — fetches first 5 + last 5 comment pages, skipping the middle. Prevents freezing on huge threads.
Lazy related refs — cross-references listed as
related_refsinstead of eagerly fetched.Unicode safety — fixed byte-slicing panics on multi-byte UTF-8 characters.
0.3.6¶
Improvements¶
Budget-based adaptive compaction —
fetch_discussion/compact_discussionnow start with full content and only compact what’s needed to fit within a byte budget (default 60KB). Small/medium PRs return fully expanded with no user effort. Large PRs gracefully degrade through 9 progressive tiers (bot filter → code blocks → comments → large patches → body → reviews → all patches → aggressive).Removed
expandparameter — no longer needed. Compaction is fully automatic based on size constraints. Compacted content is always available viaelement_iddrill-down.Per-item budget (default 15KB) prevents any single patch, comment, or body from consuming more than 25% of the total budget, ensuring balanced output even when one file dominates a PR.
budget/item_budgetparameters oncompact_discussionfor power users who want to tune output size._compactionmetadata in output describes what was compacted and at which tier, so callers know what to drill into.
0.3.5¶
Improvements¶
Smart diff sizing — small PR diffs (≤200 total lines) are shown inline with per-file collapsing for large individual patches. Large diffs show a navigation tree with
+/-counts; drill into specific files viaelement_id="patch_N". All patches are always cached regardless of size.Review comments as
review_Ncache elements — inline review comments are now individually drillable viaelement_id="review_3"withgrepandlinessupport. Previews extended from 1 line/120 chars to 3 lines/300 chars.
0.3.4¶
Fixes¶
list_dirannotate callback paths — the callback now receives paths relative torelative_to(e.g.src/core/engine.py) instead of bare filenames (engine.py). This was a bug that made it impossible to match callback paths against external data sources (knowledge graphs, databases) when listing subdirectories.
0.3.3¶
Improvements¶
PR diffs as collapsed cache elements — when fetching a PR via
ElementCache.fetch_discussion, diff patches are automatically collapsed intopatch_Ncache elements. Each stores filename, additions/deletions, and full diff text. Drill into specific files withelement_id="patch_3", search within patches withgrep="pattern", or slice withlines="10-30". Fits the existing progressive-disclosure pattern alongsidecb_N,comment_N, anddetails_N.refreshflag onfetch_discussion— subsequent calls for the same(repo, number)now return a cached summary instead of re-fetching. Passrefresh=Trueto force a fresh fetch when the discussion has changed.Removed
git_diff— PR diffs are better served as collapsed elements infetch_discussion(progressive disclosure). For comparing tags/branches outside a PR context, usegit_api("compare/v1.0...v2.0").Removed
globsetdirect dependency (was only used bygit_diff)
0.3.2¶
Improvements¶
git_diffGitHub API fallback — when localgit difffails (shallow clones, missing refs), automatically falls back to the GitHub compare API (/repos/{repo}/compare/{base}...{head}). Repo is auto-detected from git remote or can be passed explicitly via the newrepoparameter.
0.3.1¶
Fixes¶
Fixed CI import check referencing removed
git_issueexportFixed stale
git_issuereference inElementCacheerror message
0.3.0¶
Breaking changes¶
Renamed
head_limit→max_resultsacross the API. Inripgrep(), the parameter is nowmax_results. Inripgrep_files(), the oldmax_results(engine-level early termination) is nowmatch_limit, andhead_limit(output truncation) is nowmax_results.Renamed
git_issue→github_discussions. Now supports both fetching a single discussion and listing discussions with filters.Renamed
ElementCache.fetch_issue→ElementCache.fetch_discussion.
New features¶
github_discussionslisting mode — list issues, PRs, or both withkind,state,sort,limit, andlabelsfilters. Auto-detects repo from git remote whenrepois omitted.list_dirannotate callback — optionalannotateparameter accepts a callable that receives each entry’s relative path and returns an annotation string (e.g."(144 loc)"). The tree formatter handles column alignment within each directory level.
0.2.11¶
New features¶
list_dir— tree-formatted directory listing with depth control, glob filtering,.gitignoresupport, directory summaries, andrelative_tofor path display.
Performance optimizations¶
Eliminated double filesystem walk in
list_dir(merged into single walk at depth+1)Replaced HashSet+sort+dedup with O(n) sorted merge for context line merging in grep
Thread-local buffering with
FlushGuardDrop pattern in parallel walker (one lock per thread instead of per file)Thread-local single-entry regex cache in
json_grepandcachemodulesPre-canonicalize
allowed_dirsinread_fileto reduce filesystem stat callseq_ignore_ascii_case()replacing.to_lowercase()allocation in HTML tag detectionLazyLock<HashSet<&'static str>>for zero-allocation default skip directories
0.2.10¶
Bug fixes¶
Fixed
head_limitsemantics: changed fromint(0 = unlimited) toint | None(None = unlimited)Added
relative_toparameter toripgrep()wrapper
0.2.9¶
Bug fixes¶
Removed default
max_resultslimit that silently truncated search results
0.2.8¶
Changes¶
Renamed all
grepmethods toripgrepacross the codebaseDropped deprecated
macos-13CI runners from wheel build workflowRemoved tracked
__pycache__files from git
0.2.7¶
Changes¶
Fixed wheel build workflow for cross-platform distribution
0.2.5¶
Improvements¶
Searcher reuse in transform (callback) path — build searcher and sink once, reuse across files
Fast path for no-context matches in
format_content(skip HashSet/sort/dedup)Context merge moved inside the context branch to avoid allocation when unused
0.2.3¶
Major rewrite — Rust conversion¶
Rewrote core from pure Python to Rust via PyO3/maturin
grep: Uses
grep-regex,grep-searcher, andignorecrates (parallel file walking, mmap, SIMD literal optimization,.gitignoresupport)GitHub integration:
git_issue,git_api,ElementCachewith drill-down caching, text compactionFile I/O:
read_filewith path traversal protectionCompaction:
compact_discussion,collapse_code_blocks,compact_textAdded
ripgrep()Claude Code Grep-compatible wrapperAdded CI (cargo fmt, clippy, pytest) and wheel build workflows
0.1.1¶
Packaging fix
0.1.0¶
Initial release — pure Python implementation