Skip to content

Commit 24f6930

Browse files
committed
fix(monorepo): align get_context tool contract
1 parent c9c7cc3 commit 24f6930

File tree

10 files changed

+56
-26
lines changed

10 files changed

+56
-26
lines changed

apps/api/src/ailss_api/mcp_runtime.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def create_mcp_server_from_env() -> FastMCP:
5555
openai_client=OpenAI(api_key=openai_api_key),
5656
diagnostics=ToolFailureDiagnostics(vault_path=settings.resolved_vault_path, cwd=Path.cwd()),
5757
enable_write_tools=os.environ.get("AILSS_ENABLE_WRITE_TOOLS", "").strip() == "1",
58-
default_top_k=max(1, min(default_top_k, 50)),
58+
default_top_k=max(1, min(default_top_k, 20)),
5959
shutdown_token=shutdown_token,
6060
)
6161

apps/api/src/ailss_api/mcp_runtime_context_tools.py

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ def get_context(
3131
path_prefix: str | None = None,
3232
tags_any: list[str] | None = None,
3333
tags_all: list[str] | None = None,
34-
top_k: Annotated[int, Field(ge=1, le=50)] = runtime.default_top_k,
35-
expand_top_k: Annotated[int, Field(ge=0, le=50)] = 5,
34+
top_k: Annotated[int, Field(ge=1, le=20)] = runtime.default_top_k,
35+
expand_top_k: Annotated[int, Field(ge=0, le=20)] = 5,
3636
hit_chunks_per_note: Annotated[int, Field(ge=1, le=5)] = 2,
3737
neighbor_window: Annotated[int, Field(ge=0, le=3)] = 1,
3838
max_evidence_chars_per_note: Annotated[int, Field(ge=200, le=20_000)] = 1500,
@@ -74,6 +74,7 @@ def run() -> dict[str, object]:
7474
runtime.settings,
7575
)
7676
results = response.results[:top_k]
77+
expanded_result_count = min(max(0, expand_top_k), len(results))
7778
return {
7879
"query": query,
7980
"top_k": top_k,
@@ -85,7 +86,7 @@ def run() -> dict[str, object]:
8586
"tags_all": normalized_tags_all,
8687
},
8788
"params": {
88-
"expand_top_k": min(max(0, expand_top_k), top_k),
89+
"expand_top_k": expanded_result_count,
8990
"hit_chunks_per_note": hit_chunks_per_note,
9091
"neighbor_window": neighbor_window,
9192
"max_evidence_chars_per_note": max_evidence_chars_per_note,
@@ -103,23 +104,33 @@ def run() -> dict[str, object]:
103104
"heading": item.heading,
104105
"heading_path": item.heading_path,
105106
"snippet": item.snippet,
106-
"evidence_text": item.evidence_text,
107-
"evidence_truncated": item.evidence_truncated,
108-
"evidence_chunks": [
109-
{
110-
"chunk_id": chunk.chunk_id,
111-
"chunk_index": chunk.chunk_index or 0,
112-
"kind": chunk.kind if chunk.kind in {"hit", "neighbor"} else "hit",
113-
"distance": chunk.distance,
114-
"heading": chunk.heading,
115-
"heading_path": chunk.heading_path,
116-
}
117-
for chunk in item.evidence
118-
],
107+
"evidence_text": item.evidence_text
108+
if index < expanded_result_count
109+
else None,
110+
"evidence_truncated": (
111+
item.evidence_truncated if index < expanded_result_count else False
112+
),
113+
"evidence_chunks": (
114+
[
115+
{
116+
"chunk_id": chunk.chunk_id,
117+
"chunk_index": chunk.chunk_index or 0,
118+
"kind": (
119+
chunk.kind if chunk.kind in {"hit", "neighbor"} else "hit"
120+
),
121+
"distance": chunk.distance,
122+
"heading": chunk.heading,
123+
"heading_path": chunk.heading_path,
124+
}
125+
for chunk in item.evidence
126+
]
127+
if index < expanded_result_count
128+
else []
129+
),
119130
"preview": item.preview,
120131
"preview_truncated": item.preview_truncated,
121132
}
122-
for item in results
133+
for index, item in enumerate(results)
123134
],
124135
}
125136

apps/api/tests/test_mcp_runtime.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from pathlib import Path
66
from typing import Any, cast
77

8+
import pytest
89
from mcp.server.fastmcp import FastMCP
910
from openai import OpenAI
1011
from pytest import MonkeyPatch
@@ -320,13 +321,26 @@ def test_mcp_runtime_read_tools_use_python_index_and_vault(
320321
path_prefix="docs/",
321322
tags_any=["project"],
322323
top_k=2,
324+
expand_top_k=1,
323325
)
324326
results = cast(list[dict[str, object]], context["results"])
327+
params = cast(dict[str, object], context["params"])
325328
assert len(results) == 2
326329
assert {cast(str, item["path"]) for item in results} == {
327330
"docs/Child.md",
328331
"docs/Parent.md",
329332
}
333+
assert params["expand_top_k"] == 1
334+
assert sum(1 for item in results if item["evidence_text"] is not None) == 1
335+
assert sum(1 for item in results if item["evidence_chunks"]) == 1
336+
337+
with pytest.raises(Exception, match="less than or equal to 20"):
338+
_call_tool(
339+
harness.server,
340+
"get_context",
341+
query="child dependency",
342+
top_k=21,
343+
)
330344
finally:
331345
harness.close()
332346

docs/01-overview.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Example tools:
3535
Read-first tools (implemented in this repo):
3636

3737
- `get_context`: semantic retrieval for a query → returns top matching notes (deduped by path) with note metadata and stitched evidence chunks
38-
- Default `top_k` can be set via `AILSS_GET_CONTEXT_DEFAULT_TOP_K` (applies only when the caller omits `top_k`; clamped to 1–50; default: 10)
38+
- Default `top_k` can be set via `AILSS_GET_CONTEXT_DEFAULT_TOP_K` (applies only when the caller omits `top_k`; clamped to 1–20; default: 10)
3939
- Returns note metadata + stitched evidence chunks by default (file-start previews are disabled unless explicitly enabled)
4040
- Default `max_chars_per_note` is 800 (applies only when the caller omits it; clamped to 200–50,000; used for file-start previews when enabled)
4141
- Optional scoped candidate filters are available: `path_prefix` (literal path prefix), `tags_any`, `tags_all`

docs/ops/local-dev.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ Notes:
8080
Optional:
8181

8282
- `AILSS_ENABLE_WRITE_TOOLS=1` (enables explicit write tools like `edit_note`)
83-
- `AILSS_GET_CONTEXT_DEFAULT_TOP_K=<n>` (sets the default `get_context.top_k` when the caller omits `top_k`; clamped to 1–50; default: 10)
83+
- `AILSS_GET_CONTEXT_DEFAULT_TOP_K=<n>` (sets the default `get_context.top_k` when the caller omits `top_k`; clamped to 1–20; default: 10)
8484

8585
## 5) Run Python backend (FastAPI)
8686

docs/reference/mcp-tools.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ Source of truth: `apps/api/src/ailss_api/mcp_runtime.py`.
1414
- `path_prefix` (string, optional) — literal vault-relative prefix match for candidate notes (not SQL wildcard semantics)
1515
- `tags_any` (string[], default: `[]`) — candidate notes must include at least one tag
1616
- `tags_all` (string[], default: `[]`) — candidate notes must include all tags
17-
- `top_k` (int, default: `10`, range: `1–50`)
18-
- `expand_top_k` (int, default: `5`, range: `0–50`) — how many of the top_k notes include stitched evidence text
17+
- `top_k` (int, default: `10`, range: `1–20`)
18+
- `expand_top_k` (int, default: `5`, range: `0–20`) — how many of the top_k notes include stitched evidence text
1919
- `hit_chunks_per_note` (int, default: `2`, range: `1–5`)
2020
- `neighbor_window` (int, default: `1`, range: `0–3`) — stitches ±window around the best hit
2121
- `max_evidence_chars_per_note` (int, default: `1500`, range: `200–20,000`)

packages/obsidian-plugin/src/settingsSections/openAiSection.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export function renderOpenAiSection(
4848

4949
new Setting(containerEl)
5050
.setName("Top K")
51-
.setDesc("Default get_context.top_k when the caller omits top_k (1–50).")
51+
.setDesc("Default get_context.top_k when the caller omits top_k (1–20).")
5252
.addText((text) => {
5353
text.setPlaceholder("10");
5454
text.setValue(String(plugin.settings.topK));

packages/obsidian-plugin/src/utils/clamp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ function clampTopKWithMax(input: number, max: number): number {
88
}
99

1010
export function clampTopK(input: number): number {
11-
return clampTopKWithMax(input, 50);
11+
return clampTopKWithMax(input, 20);
1212
}
1313

1414
export function clampPythonApiDefaultTopK(input: number): number {

packages/obsidian-plugin/test/mcp/mcpHttpServiceController.startupHelpers.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,9 +191,9 @@ describe("McpHttpServiceController startup helper unit branches", () => {
191191
asInternals(controller).normalizeStartupSettings(settings),
192192
).resolves.toEqual({
193193
port: 31415,
194-
topK: 50,
194+
topK: 20,
195195
});
196-
expect(settings.topK).toBe(50);
196+
expect(settings.topK).toBe(20);
197197
expect(saveSettings).toHaveBeenCalledTimes(1);
198198
});
199199

packages/obsidian-plugin/test/utils/clamp.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import { describe, expect, it } from "vitest";
22

33
import {
4+
clampTopK,
45
clampPythonApiAgentTopK,
56
clampPythonApiDefaultTopK,
67
clampPythonApiPort,
78
} from "../../src/utils/clamp.js";
89

910
describe("Python clamp helpers", () => {
11+
it("caps MCP retrieval defaults at the retrieval maximum", () => {
12+
expect(clampTopK(80)).toBe(20);
13+
});
14+
1015
it("uses the Python backend fallback port", () => {
1116
expect(clampPythonApiPort(70000)).toBe(8787);
1217
});

0 commit comments

Comments
 (0)