Skip to content

Commit 8cfaaf4

Browse files
jgravelleclaude
andcommitted
v1.108.70: bounded-source mode for get_symbol_source (#340)
Add an optional, explicitly-labeled source slice to get_symbol_source so a large symbol or heavy symbol_ids batch can't hand the agent a silently partial body when the client/model context clips the response after a successful call. The source-retrieval analogue of search_symbols(token_budget=) from #328. New optional params: source_start_line/source_end_line (absolute file lines, clamped to the symbol body), max_source_lines, max_source_bytes (UTF-8-safe), and max_total_source_bytes (batch cap; oversized symbols come back partial, never dropped). When a bound shortens the body, the entry carries server-authored source_truncated / source_range / source_total_range / source_total_lines / source_total_bytes / source_truncated_reason / source_is_bounded_view. Contract (per the issue's accepted scope): - default (no bound) is byte-for-byte unchanged; bounded fields appear only when a bound is requested; - verify=true still hashes the full indexed body; a bounded entry is flagged source_is_bounded_view so the slice is never mistaken for the verified bytes; - context_lines + any bound is rejected (cannot expand past the bound); - max_total_source_bytes batch cap. Pure helpers _utf8_safe_truncate + _bound_source (range -> max_lines -> max_bytes -> batch-remaining precedence). Bounded params hidden under compact_schemas (still callable) to protect core_compact (3969, under 4000). schema_baseline.json refreshed. New tests/test_v1_108_70.py (19). Full suite 4690 passed / 10 skipped. Reported by @mmashwani. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e4ea9f6 commit 8cfaaf4

9 files changed

Lines changed: 556 additions & 26 deletions

File tree

CHANGELOG.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,54 @@
22

33
All notable changes to jcodemunch-mcp are documented here.
44

5+
## [1.108.70] - 2026-06-22 - Bounded-source mode for get_symbol_source (#340)
6+
7+
### Added
8+
9+
- **An optional, explicitly-labeled source slice for large symbols and broad
10+
batches.** `get_symbol_source` always returned the full indexed body, which is
11+
the right default but fragile when a very large symbol (or a heavy `symbol_ids`
12+
batch) gets clipped by the MCP client or model context *after* a successful
13+
call, leaving the agent with a silently-partial implementation. Bounded mode
14+
gives callers a server-side truncation contract instead:
15+
- `source_start_line` / `source_end_line` — absolute file line numbers (same
16+
frame as `line` / `end_line`), clamped to the symbol body;
17+
- `max_source_lines` — keep at most the first N lines of the (ranged) slice;
18+
- `max_source_bytes` — UTF-8-safe per-symbol byte cap;
19+
- `max_total_source_bytes` — batch cap across all returned symbols, so an
20+
oversized batch returns bounded entries instead of an N×per-symbol blowup;
21+
oversized symbols come back **partial, never dropped**.
22+
- **Server-authored truncation metadata, on the slice.** When a bound shortens
23+
the body, each entry carries `source_truncated`, `source_range`,
24+
`source_total_range`, `source_total_lines`, `source_total_bytes`,
25+
`source_truncated_reason`, and `source_is_bounded_view` — so an agent (or a
26+
verified entry) can tell the returned `source` is a slice, not the full bytes.
27+
28+
### Contract
29+
30+
- **Default is byte-for-byte unchanged.** With no bound supplied, the response
31+
and its fields are exactly as before — the bounded fields appear only when a
32+
bound is requested.
33+
- **`verify` stays full-body.** `verify=true` continues to hash the complete
34+
indexed body; a bounded entry is additionally flagged `source_is_bounded_view`
35+
so the returned source is never mistaken for the verified bytes.
36+
- **`context_lines` cannot defeat the bound.** Combining `context_lines` with any
37+
source bound is rejected, so it can never silently expand the payload past the
38+
requested bound.
39+
- Bounded params are hidden under `compact_schemas` (still callable) to protect
40+
the core_compact budget; the full surface advertises them.
41+
42+
### Tests
43+
44+
- `tests/test_v1_108_70.py` (19): UTF-8-safe truncation, range clamping, line /
45+
byte / batch caps and reason precedence (pure), plus the integration contract
46+
(default unchanged, line/range bounds + metadata, verify-still-full, batch
47+
partial-not-dropped, context_lines rejection, invalid-bound rejection).
48+
49+
The source-retrieval analogue of the `search_symbols(token_budget=...)` bounding
50+
from #328. Scoped to `tools/get_symbol.py` + the `server.py` schema. Reported by
51+
@mmashwani.
52+
553
## [1.108.69] - 2026-06-21 - Durable-change delivery: get_delivery_metrics / delivery CLI
654

755
### Added

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# jcodemunch-mcp — Project Brief
22

33
## Current State
4+
- **Version:** 1.108.70 — Bounded-source mode for `get_symbol_source` (#340, reported by @mmashwani; OP went AWOL so self-implemented against jjg's accepted-in-principle 5-point scope ruling). `get_symbol_source` always returned the full indexed body — fragile when a very large symbol or heavy `symbol_ids` batch gets clipped by the MCP client/model context AFTER a successful call (silent partial). New optional bounds: `source_start_line`/`source_end_line` (absolute file lines, clamped to the symbol body), `max_source_lines`, `max_source_bytes` (UTF-8-safe), `max_total_source_bytes` (batch cap — oversized symbols come back PARTIAL not dropped). When a bound shortens the body the entry carries server-authored `source_truncated`/`source_range`/`source_total_range`/`source_total_lines`/`source_total_bytes`/`source_truncated_reason`/`source_is_bounded_view`. **Contract held to jjg's 5 points:** (1) default with no bound is byte-for-byte unchanged (bounded fields appear only when requested); (2) metadata authored on the slice, absolute line frame; (3) `verify=true` still hashes the FULL body, bounded entry flagged `source_is_bounded_view` so the slice isn't mistaken for verified bytes; (4) `context_lines` + any bound is REJECTED (can't silently expand past the bound); (5) `max_total_source_bytes` batch cap. New pure helpers `_utf8_safe_truncate` + `_bound_source` (range→max_lines→max_bytes→batch-remaining precedence, later/tighter bound wins the reason) in `tools/get_symbol.py`; `source` var stays the full body so verify is unaffected. Bounded params added to `_COMPACT_STRIP_PARAMS["get_symbol_source"]` (hidden under compact, still callable) to protect core_compact (now 3969, under §10 ≤4000). `schema_baseline.json` refreshed (compact keys ~unchanged; *_full grew for the new params). The source-retrieval analogue of `search_symbols(token_budget=)` from #328. Scoped to `tools/get_symbol.py` + `server.py` schema. New `tests/test_v1_108_70.py` (19). Full suite 4690 passed / 10 skipped.
45
- **Version:** 1.108.69 — Durable-change delivery (`get_delivery_metrics` tool + `delivery` CLI). The suite shows tokens used + tokens **saved** (input); it had no honest **output** measure — the gap that lets teams reward raw volume (tokenmaxxing) over outcomes. New `tools/get_delivery_metrics.py` mines local git over a window and classifies each non-merge commit into ONE mutually-exclusive bucket by precedence: `revert_authored` (undoes work) → `reverted` (a later commit reverts it) → `reworked` (a file it touched was re-touched by a later commit within `rework_horizon_days` = churn-back) → `durable` (landed and stuck). `commits_durable` is the honest **numerator for a cost-per-outcome ratio** — divide AI spend over the same window by it. **Hub-file exclusion is the honesty fix:** files co-touched by ≥max(4, 20%) of window commits (CHANGELOG, version, a monolithic dispatch module) are shared ledgers, not rework evidence, so they're dropped from the rework signal (auditable via `_meta.hub_files_excluded`); without it, hot files flagged 93% of this repo's own commits as rework, with it 38%. Durability is **trailing** — commits inside the horizon are surfaced as `commits_provisional` (counted durable, not yet settled). Reuses `get_symbol_provenance._classify_commit` for the `by_category` breakdown; pure read via `_run_git` (no index write, no new tables). Diagnostic trend, NOT a score to chase (rewarding the number re-invents the proxy-gaming it replaces). `delivery [repo] [--window-days] [--rework-horizon-days] [--cost DOLLARS] [--json]` — `--cost` prints the headline cost-per-durable-change. Standard tier (alongside get_churn_rate/get_hotspots), so `core_compact` unaffected. Tool count 84→85 (full). New `tests/test_delivery_metrics.py` (7: bucket partition, hub exclusion, horizon-zero, honest no-git/empty-window) with date-controlled commits (same-second commits dodge the `0 < delta` rework check). The numerator half; the jMunch Console cost-per-outcome **tile** (join its window token-spend with this) is the buildable follow-up. See memory `project_jmunch_console`.
56
- **Version:** 1.108.68 — Retrieval-regret loop (`suggest_corrections` tool + `reflect` CLI). The `ranking_events` telemetry ledger fed only `WeightTuner`; it also carries an unread, louder signal — when retrieval failed and the agent had to re-ask. New `retrieval/regret.py::analyze_regret` mines SIX regret signals over the ledger (re-query churn, low confidence, thin/zero result, ambiguous top, stale-at-query, vocabulary gap = identity-miss-rescued-by-semantic) as severity-ranked clusters (pure read via the existing `token_tracker.ranking_db_query` loader; no new tables). `tools/suggest_corrections.py` fuses clusters + `audit_agent_config` stale-config findings + a dry-run `WeightTuner` proposal into prioritized, explainable corrections: CLAUDE.md routing/glossary lines as **unified-diff previews** (difflib), index-freshness hints (reads `get_watch_status` service.active), the weight proposal. **Charter: NEVER writes a user file** — suggestions only, applying a patch is the user's keystroke; the only persistable state is the `tuning.jsonc` weights sidecar behind explicit `apply_weights`. Honest no-telemetry hint when `perf_telemetry_enabled` off (ledger only exists then). Clean-room "suggest, don't write" is the differentiator ([[feedback_jcm_read_only_charter]]). Surfaces: `suggest_corrections` MCP tool (standard tier, alongside tune_weights/analyze_perf), `reflect` CLI (`reflect [repo] [--all] [--apply-weights] [--json]`), one-line digest integration. Tool count 83→84 (full) / standard +1. `schema_baseline.json` refreshed (core_compact still 3956, suggest_corrections is standard not core). New `tests/test_suggest_corrections.py` (12) incl. the charter no-write gate. See memory `project_retrieval_regret_prd`.
67
- **Version:** 1.108.67 — Demote the `language` enum under `compact_schemas`. The `language` filter's full LANGUAGE_REGISTRY enum (~76 values, ~200 tokens of mechanical names an agent already knows) is dropped to a plain `type:string` filter under compact (capability preserved — the tool accepts any language string; unknown → empty match); the full surface keeps the enum. Brings `core_compact` back under the §10 ≤4000 line (4162→**3956**, ~44 margin) after 1.108.55-66 tool additions pushed it to 4146; also trims `standard_compact` (16339→16149). New `_COMPACT_DEMOTE_ENUM_PARAMS` frozenset (keyed by PARAM name, so search_symbols/search_ast/etc. all benefit) + an enum-demotion pass alongside the existing `_COMPACT_STRIP_PARAMS` loop in `_build_tools_list`. `schema_baseline.json` refreshed to honest live counts (the wholesale `capture_schema_baseline.py` run is now CORRECT — it writes 3956, the very thing that made it wrong at v1.108.66 when it wrote 4146). New `test_compact_demotes_language_enum_keeps_capability` pins it (enum gone under compact, present in full). Closes the open follow-up from the Counter release. No behavior change on the full tier.
@@ -116,7 +117,7 @@ src/jcodemunch_mcp/
116117
tools/
117118
index_folder.py # Local indexer (sync → asyncio.to_thread in server.py). v1.108.0 adds `paths=[...]` arg via new `resolve_explicit_paths()` helper to skip the directory walk when the caller supplies an explicit file/subdir list; security matches the walk path (outside-root / traversal / symlink-escape / oversize / unsupported-ext all warn-and-skip with per-entry warnings). v1.108.6 adds `identity_mode: "config"|"local"|"git"` arg — delegates to `storage/git_root.resolve_index_identity()` which is the single source of truth for local-folder → repo-ID resolution (replacing duplicated logic across watcher.py / resolve_repo.py / index_folder.py).
118119
index_repo.py # GitHub indexer (async, httpx)
119-
get_symbol.py # get_symbol_source: shape-follows-input (id→flat, ids[]→{symbols,errors})
120+
get_symbol.py # get_symbol_source: shape-follows-input (id→flat, ids[]→{symbols,errors}). v1.108.70 bounded-source mode: optional source_start_line/source_end_line/max_source_lines/max_source_bytes/max_total_source_bytes return an explicitly-labeled slice (source_truncated + range/total metadata, source_is_bounded_view); verify stays full-body; context_lines+bound rejected. Pure helpers _utf8_safe_truncate + _bound_source
120121
search_columns.py # Column search across dbt/SQLMesh models
121122
get_context_bundle.py # Symbol + imports bundle; token_budget/budget_strategy
122123
get_ranked_context.py # Query-driven budgeted context (BM25 + PageRank)

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,9 @@ is a byte the agent doesn't pay to read.
110110
<!-- WHATSNEW:START -->
111111
#### What's new
112112

113-
- **[v1.108.61](https://github.com/jgravelle/jcodemunch-mcp/releases/tag/v1.108.61)** (2026-06-18) — Every config key is now documented (no more "Other")
114-
- **[v1.108.60](https://github.com/jgravelle/jcodemunch-mcp/releases/tag/v1.108.60)** (2026-06-18) — `config --check` distinguishes sandbox-limited storage probes
115-
- **[v1.108.59](https://github.com/jgravelle/jcodemunch-mcp/releases/tag/v1.108.59)** (2026-06-17) — Decision-context surfacing in impact analysis
113+
- **[v1.108.70](https://github.com/jgravelle/jcodemunch-mcp/releases/tag/v1.108.70)** (2026-06-22) — Bounded-source mode for get_symbol_source (#340)
114+
- **[v1.108.69](https://github.com/jgravelle/jcodemunch-mcp/releases/tag/v1.108.69)** (2026-06-21) — Durable-change delivery: get_delivery_metrics / delivery CLI
115+
- **[v1.108.68](https://github.com/jgravelle/jcodemunch-mcp/releases/tag/v1.108.68)** (2026-06-21) — Retrieval-regret loop: suggest_corrections / reflect
116116
<!-- WHATSNEW:END -->
117117

118118
![License](https://img.shields.io/badge/license-dual--use-blue)

benchmarks/schema_baseline.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
2-
"core_compact": 3956,
3-
"core_full": 5097,
4-
"standard_compact": 16407,
5-
"standard_full": 17618,
6-
"full_compact": 17717,
7-
"full_full": 18928
2+
"core_compact": 3969,
3+
"core_full": 5328,
4+
"standard_compact": 16703,
5+
"standard_full": 18132,
6+
"full_compact": 18013,
7+
"full_full": 19442
88
}

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "jcodemunch-mcp"
3-
version = "1.108.69"
3+
version = "1.108.70"
44
description = "Token-efficient MCP server for source code exploration via tree-sitter AST parsing"
55
readme = "README.md"
66
requires-python = ">=3.10"

src/jcodemunch_mcp/server.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,13 @@ async def _apply_model_announcement(model: str) -> dict:
566566
"fuzzy", "fuzzy_threshold", "max_edit_distance", "sort_by", "fqn",
567567
"decorator", "token_budget",
568568
},
569+
# Bounded-source mode is an advanced opt-in; the tool still accepts these
570+
# params under compact, they're just hidden from the schema to protect the
571+
# core_compact budget (the body is always callable with them).
572+
"get_symbol_source": {
573+
"source_start_line", "source_end_line", "max_source_lines",
574+
"max_source_bytes", "max_total_source_bytes",
575+
},
569576
"get_context_bundle": {"budget_strategy"},
570577
"get_ranked_context": {"detail_level"},
571578
"get_blast_radius": {"cross_repo", "max_depth"},
@@ -1316,7 +1323,7 @@ def _build_tools_list() -> list[Tool]:
13161323
),
13171324
Tool(
13181325
name="get_symbol_source",
1319-
description="Get full source of one symbol (symbol_id → flat object) or many (symbol_ids[] → {symbols, errors}). Supports verify, context_lines, and fqn (PHP FQN via PSR-4).",
1326+
description="Get full source of one symbol (symbol_id → flat object) or many (symbol_ids[] → {symbols, errors}). Supports verify, context_lines, fqn (PHP FQN via PSR-4), and an optional bounded mode that caps returned source for large symbols/batches.",
13201327
inputSchema={
13211328
"type": "object",
13221329
"properties": {
@@ -1352,6 +1359,26 @@ def _build_tools_list() -> list[Tool]:
13521359
"fqn": {
13531360
"type": "string",
13541361
"description": "PHP fully-qualified class name (e.g. 'App\\Models\\User'). Resolves to symbol_id via PSR-4. Alternative to symbol_id."
1362+
},
1363+
"source_start_line": {
1364+
"type": "integer",
1365+
"description": "Bounded mode: absolute file line (1-based, same frame as `line`/`end_line`) to start the returned source slice; clamped to the symbol body."
1366+
},
1367+
"source_end_line": {
1368+
"type": "integer",
1369+
"description": "Bounded mode: absolute file line (1-based, inclusive) to end the returned source slice; clamped to the symbol body."
1370+
},
1371+
"max_source_lines": {
1372+
"type": "integer",
1373+
"description": "Bounded mode: keep at most the first N lines of the (ranged) slice. Sets source_truncated + metadata when it shortens the body."
1374+
},
1375+
"max_source_bytes": {
1376+
"type": "integer",
1377+
"description": "Bounded mode: UTF-8-safe per-symbol byte cap on the returned source. Verify still hashes the full body."
1378+
},
1379+
"max_total_source_bytes": {
1380+
"type": "integer",
1381+
"description": "Bounded mode (batch): cap on total returned source bytes across all symbols. Oversized symbols come back partial (source_truncated) rather than dropped, preventing an N×per-symbol blowup."
13551382
}
13561383
},
13571384
"required": ["repo"]
@@ -4365,6 +4392,11 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
43654392
context_lines=arguments.get("context_lines", 0),
43664393
storage_path=storage_path,
43674394
fqn=arguments.get("fqn"),
4395+
source_start_line=arguments.get("source_start_line"),
4396+
source_end_line=arguments.get("source_end_line"),
4397+
max_source_lines=arguments.get("max_source_lines"),
4398+
max_source_bytes=arguments.get("max_source_bytes"),
4399+
max_total_source_bytes=arguments.get("max_total_source_bytes"),
43684400
)
43694401
)
43704402
elif name == "search_symbols":

0 commit comments

Comments
 (0)