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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -398,8 +398,17 @@ sanitized, bounded fields. Add `api_version` when introducing or auditing a
public top-level CLI JSON DTO. MCP JSON-RPC uses `McpServer`'s camelCase options for the
protocol envelope while tool structured content keeps its documented
machine-readable keys. Every object-shaped tool `structuredContent` envelope carries
root-level `api_version`, injected by `CreateToolResult`; sanitize/redact values before mutating `JsonObject` /
`JsonNode` instances. LSP, quickfix, and SARIF outputs follow their external
root-level `api_version`, injected by the success and typed-error response builders; sanitize/redact values before mutating `JsonObject` /
`JsonNode` instances. Every full `tools/list` definition also publishes a draft
2020-12 `outputSchema` generated by `McpToolOutputSchemas`; its reusable definitions
cover versioned success envelopes, rows, readiness, pagination/truncation, warnings,
and typed tool errors. Success and typed-error variants require a per-tool `tool`
discriminator with a tool-name `const`, typed errors carry the same root `api_version`,
and open compatibility values remain bounded by finite nesting plus property,
array-item, and string-length limits. Keep field names and nesting
aligned with actual structured results, and keep the tool-name switch exhaustive so a
newly registered structured tool cannot ship without an output contract. Compact catalog entries remain
definition-incomplete and direct clients to the full catalog. LSP, quickfix, and SARIF outputs follow their external
schemas rather than the CLI snake_case contract. GitHub/report helpers and
worker/private storage paths use their own bounded serializers because they are
either API clients, persisted local state, or process-internal protocols. The
Expand Down Expand Up @@ -3827,8 +3836,16 @@ DOM で組み立てる `JsonObject` payload は sanitized / bounded 済み field
公開 top-level CLI JSON DTO を追加または audit するときは `api_version` を追加してください。MCP JSON-RPC は
protocol envelope に `McpServer` の camelCase option を使い、tool structured content は文書化済みの
machine-readable key を保ちます。object 形式のすべての tool `structuredContent` envelope は
`CreateToolResult` が追加する root-level `api_version` を持ちます。`JsonObject` / `JsonNode` を mutate する前に値を sanitize /
redact してください。LSP、quickfix、SARIF 出力は CLI snake_case contract ではなく外部 schema に
success / typed-error response builder が追加する root-level `api_version` を持ちます。`JsonObject` / `JsonNode` を mutate する前に値を sanitize /
redact してください。full `tools/list` の各 definition は `McpToolOutputSchemas` が生成する draft 2020-12
`outputSchema` も公開し、再利用可能な definition で version 付き success envelope、row、readiness、
pagination / truncation、warning、型付き tool error を表します。success / typed-error variant は
tool 名の `const` を持つ tool ごとの `tool` discriminator を必須とし、typed error も同じ root
`api_version` を持ちます。互換性のための open な値は有限の nesting と property 数、array item 数、
string 長の上限で bounded に保ちます。field 名と nesting を実際の structured
result に合わせ、新しく登録した structured tool が output contract なしで出荷されないよう、tool-name switch は
網羅的に保ってください。compact catalog entry は
引き続き definition-incomplete とし、client を full catalog へ案内します。LSP、quickfix、SARIF 出力は CLI snake_case contract ではなく外部 schema に
従います。GitHub/report helper と worker/private storage path は API client、永続化ローカル状態、
process-internal protocol のいずれかなので、それぞれの bounded serializer を使います。
`LocalJsonlJsonWriterOptions` の relaxed encoder は private append-only JSONL diagnostic 専用であり、
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,11 @@ incomplete generation; JSON still reports `status: "partial"`.
versioned surfaces are the `cdidx` CLI, CLI JSON output, and `cdidx mcp`
JSON-RPC interface. There is no public library / SDK API. See
[INTEGRATION_POLICY.md](INTEGRATION_POLICY.md#api-surface-and-library-use).
Full MCP `tools/list` definitions include bounded draft 2020-12 `outputSchema`
contracts with a required per-tool `tool` discriminator for structured success,
partial, and versioned typed-error results. Open compatibility values have finite
nesting plus property, array-item, and string-length bounds; compact catalogs point
clients to the full definitions instead of duplicating those schemas.

## CLI JSON Error Contract

Expand Down Expand Up @@ -697,6 +702,10 @@ commit し、構造化 `file_errors` を返して partial-result 終了コード
バージョニング契約の対象は、`cdidx` CLI、CLI JSON 出力、`cdidx mcp` の
JSON-RPC interface です。公開 library / SDK API は提供していません。詳細は
[INTEGRATION_POLICY.md](INTEGRATION_POLICY.md#api-surface-and-library-use) を参照してください。
MCP の full `tools/list` definition は、tool ごとに必須の `tool` discriminator を持つ structured
success、partial、version 付き typed-error result 用の bounded な draft 2020-12 `outputSchema`
contract を含みます。open な互換値にも有限の nesting と property 数、array item 数、string 長の
上限を設け、compact catalog は schema を重複させず client を full definition へ案内します。

## CLI JSON エラー契約

Expand Down
2 changes: 2 additions & 0 deletions TESTING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Use the full suite by default. Use targeted filters only while iterating locally
- These test-only packages are separate from the production dependency rule in `src/CodeIndex`, which still allows only `Microsoft.Data.Sqlite` at runtime.
- `FsCheck.Xunit` is reserved for property-based tests that assert universal invariants (never-throws contracts, idempotence, "output is parseable by downstream consumer") across randomly generated inputs. Use it to complement, not replace, the example-based `[Fact]` / `[Theory]` tests — pick FsCheck when the property is a universally quantified claim, and an example test when a specific concrete case is the contract.
- Test parallelism: enabled by default across independent test classes. Tests that touch process-global state such as SQLite pool resets, environment variables, or current-directory overrides must use an explicit non-parallel collection. Console-sensitive classes share one non-parallel xUnit collection, so they remain serial with each other and do not run beside independent classes that may write request-id or global diagnostics to the process console. Use `ConsoleCapture` for ordinary capture and keep every direct `Console.Out` / `Console.Error` swap under `TestConsoleLock.Gate`. Snapshot and assert global console writers under the same gate so another test cannot replace a writer between capture completion and the assertion. That gate aliases the production `ConsoleStreamOwnership` gate so console synchronization and scoped production redirects cannot retain a test writer after its capture ends.
- MCP structured-output schema coverage is split between the catalog assertions in `McpServerToolsListTests` and actual-result validation in `McpServerOutputSchemaTests`. It requires every full catalog entry to advertise an `outputSchema`, validates actual success and typed-error results for all 24 tools plus search-recipe, empty-excerpt, and truncated variants, and rejects incomplete, versionless-error, excessive-depth, and every cross-tool success pair with the deliberately small local evaluator. Keep the per-tool `tool` discriminator, required fields, bounds, and runtime field names/nesting in sync. Extend that evaluator only for JSON Schema keywords emitted by `McpToolOutputSchemas`; compact catalog entries intentionally omit complete schemas.
- Markdown unused-audit coverage indexes one real Markdown fixture containing common backtick and tilde fence-language markers. Keep default suppression, `documentation_surface` totals, reason tags, and `--all` recovery in that shared fixture.
- FTS optimization recommendation coverage keeps the shared evaluator exact at one write below, at, and one write above the 25-write threshold. Status, explain, optimize dry-run, optimize execution, and vacuum maintenance guidance must expose the same `recommended`, `action`, `reason`, `threshold_writes`, `observed_writes`, and `state`; stale batches, known WAL-stale snapshots, forward-incompatible schema stamps, and unavailable legacy counters/page snapshots suppress the recommendation, query-only status performs no source writes, and execution uses the focused counter/page/forward-contract/freshness snapshot instead of full status scans. A hot-WAL fixture opened through an explicit `immutable=1` URI must prove that status, standalone optimize dry-run, and the `index --optimize` dry-run alias preserve the same stale recommendation. A WAL or freelist state of `unknown` cannot select the optimize command, and a successful optimize reports the reset counter afterward.
- Full-scan CLI and MCP no-op coverage treats one repository-wide reusable-stat snapshot read and one folded-readiness verification as performance contracts. Keep assertions for one snapshot read, one stat lookup per candidate, one folded verification, and no content load for unchanged files when changing incremental indexing.
Expand Down Expand Up @@ -969,6 +970,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests"
- これらの test-only package は `src/CodeIndex` の本番依存ルールとは別であり、runtime 側は引き続き `Microsoft.Data.Sqlite` のみを許容する。
- `FsCheck.Xunit` はランダム生成入力に対する普遍的不変条件(never-throws、idempotence、"出力が downstream consumer で parse 可能" 等)を表明する property-based テスト専用です。例ベースの `[Fact]` / `[Theory]` を置き換えるのではなく補完するもので、普遍量化された主張なら FsCheck、特定の具体ケースが契約なら例ベースという形で使い分けてください。
- テスト並列実行: 独立したテストクラス間ではデフォルトで有効です。SQLite pool の解放、環境変数の変更、カレントディレクトリの上書きのような process-global 状態を触るテストは、明示的な non-parallel collection に入れてください。console-sensitive class は同じ non-parallel な xUnit collection を共有するため、互いに直列実行され、request-id や global diagnostics を process console へ書く可能性がある独立 class とも並列実行されません。通常の capture には `ConsoleCapture` を使い、`Console.Out` / `Console.Error` を直接差し替える場合は `TestConsoleLock.Gate` で保護してください。global console writer の snapshot 取得と assertion も同じ gate 内で行い、capture 完了から assertion までの間に別のテストが writer を差し替えないようにします。この gate は本番の `ConsoleStreamOwnership` gate と同一なので、console 同期処理や scoped redirect が capture 終了後も test writer を保持することを防ぎます。
- MCP structured-output schema の coverage は `McpServerToolsListTests` の catalog assertion と `McpServerOutputSchemaTests` の actual-result validation に分けます。full catalog の全 entry が `outputSchema` を公開することを要求し、全24 tool の実際の success / typed-error result に加えて search recipe、empty excerpt、truncated variant を検証し、不完全な payload、version のない error、過剰な nesting、全 tool 間の success 組み合わせを意図的に小さく保った local evaluator で拒否します。tool ごとの `tool` discriminator、required field、bound、runtime の field 名 / nesting を同期してください。この evaluator は `McpToolOutputSchemas` が出力する JSON Schema keyword に必要な場合だけ拡張し、compact catalog entry は完全な schema を意図的に省略します。
- Markdown の unused audit coverage は、一般的な backtick / tilde fence の language marker を含む実 Markdown fixture を1回 index します。同じ fixture で既定抑制、`documentation_surface` totals、reason tag、`--all` による復元を維持してください。
- FTS optimization recommendation coverage は、25 write threshold の1つ下、ちょうど、1つ上で shared evaluator の境界を固定します。status、explain、optimize dry-run、optimize execution、vacuum maintenance guidance は同じ `recommended`、`action`、`reason`、`threshold_writes`、`observed_writes`、`state` を公開し、stale batch、既知の WAL-stale snapshot、forward-incompatible な schema stamp、利用できない legacy counter / page snapshot は recommendation を抑止します。query-only status は source に書き込まず、execution は full status scan ではなく counter / page / forward-contract / freshness に限定した snapshot を使います。hot WAL fixture を明示的な `immutable=1` URI で開き、status、standalone optimize dry-run、`index --optimize` dry-run alias が同じ stale recommendation を保持することも証明します。WAL または freelist の state が `unknown` の場合は optimize command を選択せず、成功した optimize は reset 後の counter を返す必要があります。
- full-scan CLI と MCP の no-op coverage は、リポジトリ全体の reusable-stat snapshot read と folded-readiness verification がそれぞれ 1 回であることを performance contract とします。incremental indexing を変更するときは、snapshot read が 1 回、候補ごとの stat lookup が 1 回、folded verification が 1 回、unchanged file の content load が 0 回という assertion を維持してください。
Expand Down
26 changes: 26 additions & 0 deletions changelog.d/unreleased/4898.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
category: added
issues:
- 4898
affected:
- src/CodeIndex/Mcp/McpToolOutputSchemas.cs
- src/CodeIndex/Mcp/McpServer.Responses.cs
- src/CodeIndex/Mcp/McpServer.ToolDispatch.cs
- src/CodeIndex/Mcp/McpToolHandlers.BatchQuery.cs
- src/CodeIndex/Mcp/McpToolHandlers.Query.Status.cs
- src/CodeIndex/Mcp/McpToolCatalog.cs
- tests/CodeIndex.Tests/McpServerToolsListTests.cs
- tests/CodeIndex.Tests/McpServerOutputSchemaTests.cs
- tests/CodeIndex.Tests/McpServerToolsCallTests.cs
- README.md
- DEVELOPER_GUIDE.md
- TESTING_GUIDE.md
---

## English

- **MCP tools now publish typed structured-output schemas (#4898)** — Every full `tools/list` entry advertises a bounded draft 2020-12 `outputSchema` covering a per-tool `tool` discriminator, versioned success and error results, rows, readiness, pagination and truncation, warnings, and finite-depth compatibility values. Catalog construction fails when a newly registered tool lacks a schema, and regression coverage validates actual success and typed-error results for all 24 tools plus search-recipe, empty, partial, incomplete, excessive-depth, and full cross-tool cases.

## 日本語

- **MCP tool が型付き structured-output schema を公開するようになりました (#4898)** — full `tools/list` の全 entry が、tool ごとの `tool` discriminator、version 付き success / error result、row、readiness、pagination / truncation、warning、有限階層の互換値を表す bounded な draft 2020-12 `outputSchema` を公開します。新しく登録した tool に schema がない場合は catalog 構築を失敗させ、回帰テストで全24 tool の実際の success / typed-error result と search recipe、empty、partial、不完全、過剰階層、全 tool 間の case を検証します。
6 changes: 6 additions & 0 deletions src/CodeIndex/Mcp/McpServer.Responses.cs
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,8 @@ private JsonObject CreateToolResult(
private void EnrichToolStructuredContent(JsonObject structuredContent)
{
structuredContent.TryAdd("api_version", JsonOutputContract.ApiVersion);
if (_currentToolOutputName.Value is string toolName)
structuredContent.TryAdd("tool", toolName);
AddProjectFilterRootDiagnostics(structuredContent);
AddConfiguredSqliteDiagnostics(structuredContent);
}
Expand Down Expand Up @@ -475,6 +477,9 @@ private JsonObject CreateToolErrorResponse(bool hasId, JsonNode? id, string mess
{
ClearProjectFilterRootDiagnostics();
var structuredContent = McpErrorEnvelope.BuildData(category, suggestion, retrySafe, AddCorrelationData(extraData));
structuredContent["api_version"] = JsonOutputContract.ApiVersion;
if (_currentToolOutputName.Value is string toolName)
structuredContent["tool"] = toolName;
AddConfiguredSqliteDiagnostics(structuredContent);
var result = new JsonObject
{
Expand Down Expand Up @@ -510,6 +515,7 @@ private static JsonObject CreateToolDefinition(string name, string description,
["name"] = name,
["description"] = AppendLanguageSupportClause(name, description),
["inputSchema"] = inputSchema,
["outputSchema"] = McpToolOutputSchemas.Create(name),
["examples"] = BuildToolExamples(name),
};
if (annotations != null)
Expand Down
7 changes: 6 additions & 1 deletion src/CodeIndex/Mcp/McpServer.ToolDispatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace CodeIndex.Mcp;

public partial class McpServer : IDisposable
{

private readonly AsyncLocal<string?> _currentToolOutputName = new();

// Tool definitions are in McpToolDefinitions.cs / ツール定義は McpToolDefinitions.cs に分離

Expand All @@ -37,6 +37,10 @@ private async Task<JsonNode> HandleToolsCallAsync(bool hasId, JsonNode? id, Json
? parsedToolName
: null;
var observedToolName = toolName ?? "(missing)";
var previousToolOutputName = _currentToolOutputName.Value;
_currentToolOutputName.Value = toolName is not null && McpToolFilter.IsKnownTool(toolName)
? toolName
: null;

Database.DbDebug.ResetContext();
var metricsStartedAt = _timeProvider.GetUtcNow();
Expand Down Expand Up @@ -182,6 +186,7 @@ JsonObject CreateUnknownToolResponseForMetrics()
}
finally
{
_currentToolOutputName.Value = previousToolOutputName;
Database.DbDebug.ResetContext();
if (MetricsSink.IsActive)
{
Expand Down
2 changes: 1 addition & 1 deletion src/CodeIndex/Mcp/McpToolCatalog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ private static JsonArray CreateToolCatalog()
["items"] = new JsonObject { ["type"] = "string", ["minLength"] = 1, ["maxLength"] = MaxStatusProjectionFieldCharacters }
}
},
["description"] = "Return only these exact top-level structured-content fields after applying `format`, plus the standard `api_version`. Accepts one field or an array; nested paths are not supported."
["description"] = "Return only these exact top-level structured-content fields after applying `format`, plus the standard `api_version` and `tool` discriminators. Accepts one field or an array; nested paths are not supported."
}
}
},
Expand Down
Loading
Loading