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
17 changes: 17 additions & 0 deletions changelog.d/unreleased/1417.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
category: fixed
issues:
- 1417
affected:
- src/CodeIndex/Mcp/McpServer.cs
- src/CodeIndex/Mcp/McpToolHandlers.cs
- tests/CodeIndex.Tests/McpServerTests.cs
---

## English

- **MCP tool argument type mismatches now return JSON-RPC invalid params (#1417)** — wrong JSON types such as a string `limit` now produce `-32602` with structured parameter details instead of falling through to an internal/tool failure.

## 日本語

- **MCP ツール引数の型不一致が JSON-RPC invalid params を返すようになりました (#1417)** — 文字列の `limit` など誤った JSON 型は、internal/tool failure に落ちず `-32602` と構造化されたパラメータ詳細を返します。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/1469.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 1469
affected:
- src/CodeIndex/Mcp/McpServer.cs
- tests/CodeIndex.Tests/McpServerTests.cs
---

## English

- **MCP startup logs no longer expose the full DB path by default (#1469)** — the startup banner now logs only a sanitized DB filename unless `CDIDX_DEBUG=unsafe` is set.

## 日本語

- **MCP 起動ログが既定で完全な DB パスを公開しないようになりました (#1469)** — 起動バナーは `CDIDX_DEBUG=unsafe` が設定されていない限り、サニタイズ済みの DB ファイル名だけを記録します。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/1470.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 1470
affected:
- src/CodeIndex/Mcp/McpServer.cs
- tests/CodeIndex.Tests/McpServerTests.cs
---

## English

- **MCP catch-all error responses now hide exception details by default (#1470)** — unexpected tool and loop failures return generic wire messages while preserving detailed diagnostics in stderr, with verbose responses limited to `CDIDX_DEBUG=unsafe`.

## 日本語

- **MCP catch-all エラー応答が既定で例外詳細を隠すようになりました (#1470)** — 予期しないツール/ループ失敗は wire 上では汎用メッセージを返し、詳細診断は stderr に残します。詳細応答は `CDIDX_DEBUG=unsafe` の場合だけ有効です。
53 changes: 47 additions & 6 deletions src/CodeIndex/Mcp/McpServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ public partial class McpServer : IDisposable
internal const int DefaultMaxResponseBytes = 10 * 1024 * 1024;
private const string MaxResponseBytesEnvVar = "CDIDX_MCP_RESPONSE_MAX_BYTES";
private const string KeepAliveIntervalEnvironmentVariable = "CDIDX_MCP_KEEP_ALIVE_INTERVAL_S";
internal const string DebugEnvironmentVariable = "CDIDX_DEBUG";
internal const int MaxJsonDepth = 32;
internal const int MaxBatchRequestCount = 100;
// Stdio buffer for the JSON-RPC loop. Sized to fit typical large MCP payloads (e.g. batch_query)
Expand Down Expand Up @@ -436,7 +437,7 @@ internal async Task RunAsync(IMcpTransport transport, CancellationToken cancella

// Use stderr for logging so stdout stays clean for JSON-RPC
// stdoutをJSON-RPC用にクリーンに保つため、ログはstderrに出力
ConsoleUi.TryWriteErrorLine($"[cdidx-mcp] Starting MCP server v{_version} (db: {_dbPath}, transport: {transport.Name} @ {transport.Endpoint}, max in-flight: {MaxConcurrency})");
ConsoleUi.TryWriteErrorLine($"[cdidx-mcp] Starting MCP server v{_version} (db: {FormatDbPathForLog(_dbPath)}, transport: {transport.Name} @ {transport.Endpoint}, max in-flight: {MaxConcurrency})");

if (transport is HttpMcpTransport httpTransport)
{
Expand Down Expand Up @@ -2143,11 +2144,25 @@ private async Task<JsonNode> HandleToolsCallAsync(JsonNode? id, JsonNode? callPa
if (ValidateToolArguments(toolName, args) is JsonObject argumentError)
{
metricsError = "invalid_argument";
response = CreateToolErrorResponse(id, argumentError["message"]!.GetValue<string>(),
category: McpErrorEnvelope.CategoryInvalidArgument,
suggestion: "Use exactly the argument names advertised by tools/list for this tool.",
retrySafe: false,
extraData: argumentError);
if (argumentError["jsonrpc_invalid_params"] is JsonValue invalidParamsMarker
&& invalidParamsMarker.TryGetValue<bool>(out var invalidParams)
&& invalidParams)
{
argumentError.Remove("jsonrpc_invalid_params");
response = CreateErrorResponse(hasId: true, id: id, code: -32602, message: argumentError["message"]!.GetValue<string>(),
category: McpErrorEnvelope.CategoryInvalidArgument,
suggestion: "Use the JSON types advertised by tools/list for this tool.",
retrySafe: false,
extraData: argumentError);
}
else
{
response = CreateToolErrorResponse(id, argumentError["message"]!.GetValue<string>(),
category: McpErrorEnvelope.CategoryInvalidArgument,
suggestion: "Use exactly the argument names advertised by tools/list for this tool.",
retrySafe: false,
extraData: argumentError);
}
}
else if (ValidateCommonListArguments(args) is JsonObject listArgumentError)
{
Expand Down Expand Up @@ -2556,6 +2571,28 @@ internal static string BuildUnknownNotificationLog(string method) =>
internal static bool IsSupportedMcpLogLevel(string? level)
=> level is "debug" or "info" or "notice" or "warning" or "error" or "critical" or "alert" or "emergency";

internal static bool IsUnsafeDebugEnabled()
=> string.Equals(Environment.GetEnvironmentVariable(DebugEnvironmentVariable), "unsafe", StringComparison.OrdinalIgnoreCase);

internal static string FormatDbPathForLog(string dbPath)
{
if (IsUnsafeDebugEnabled())
return dbPath;

try
{
var path = dbPath;
if (Uri.TryCreate(dbPath, UriKind.Absolute, out var uri) && uri.IsFile)
path = uri.LocalPath;
var fileName = Path.GetFileName(path);
return string.IsNullOrWhiteSpace(fileName) ? "(configured db)" : fileName;
}
catch
{
return "(configured db)";
}
}

// Wire-safe error body for the tool catch-all. Mentions the tool and the
// exception type so the client can branch (retry vs. surface to user)
// while keeping bound values or matched content out of the response (#1530).
Expand All @@ -2569,6 +2606,8 @@ internal static bool IsSupportedMcpLogLevel(string? level)
// #1530 で封じた ex.Message 漏れを再現させずに失敗詳細をクライアントへ届ける。
internal static string BuildSanitizedToolErrorMessage(string toolName, Exception ex)
{
if (!IsUnsafeDebugEnabled())
return $"Tool '{toolName}' failed. See cdidx server stderr for details.";
if (ex is CodeIndexException codeIndexEx)
return $"Error executing {toolName} ({ex.GetType().Name}) [{codeIndexEx.Code}/{codeIndexEx.Category}]{BuildPathFragment(codeIndexEx)}{BuildHintFragment(codeIndexEx)}. See cdidx server stderr for details.";
return $"Error executing {toolName} ({ex.GetType().Name}). See cdidx server stderr for details.";
Expand All @@ -2579,6 +2618,8 @@ internal static string BuildSanitizedToolErrorMessage(string toolName, Exception
// JSON-RPC ループ catch-all のワイヤー向け本文。理由はツール catch-all と同じ(#1530, #1580)。
internal static string BuildSanitizedLoopErrorMessage(Exception ex)
{
if (!IsUnsafeDebugEnabled())
return "Internal MCP error. See cdidx server stderr for details.";
if (ex is CodeIndexException codeIndexEx)
return $"Internal error ({ex.GetType().Name}) [{codeIndexEx.Code}/{codeIndexEx.Category}]{BuildPathFragment(codeIndexEx)}{BuildHintFragment(codeIndexEx)}. See cdidx server stderr for details.";
return $"Internal error ({ex.GetType().Name}). See cdidx server stderr for details.";
Expand Down
79 changes: 79 additions & 0 deletions src/CodeIndex/Mcp/McpToolHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -386,9 +386,88 @@ private static List<string> ReadStringList(JsonNode? args, string propertyName)
}
}

if (ValidateToolArgumentTypes(toolName, obj) is JsonObject typeError)
return typeError;

return null;
}

private static JsonObject? ValidateToolArgumentTypes(string toolName, JsonObject args)
{
foreach (var property in args)
{
if (TryGetExpectedJsonType(toolName, property.Key, out var expected)
&& !MatchesExpectedJsonType(property.Value, expected))
{
return new JsonObject
{
["message"] = $"Invalid type for argument '{property.Key}' on tool '{toolName}'. Expected {expected}.",
["tool"] = toolName,
["parameter"] = property.Key,
["expected"] = expected,
["actual"] = DescribeJsonType(property.Value),
["jsonrpc_invalid_params"] = true,
};
}
}

return null;
}

private static bool TryGetExpectedJsonType(string toolName, string argumentName, out string expected)
{
if (argumentName is "path" or "project" or "excludePaths" or "names")
{
expected = string.Empty;
return false;
}

expected = argumentName switch
{
"limit" or "offset" or "snippetLines" or "maxLineWidth" or "before" or "after" or
"focusLine" or "focusColumn" or "focusLength" or "startLine" or "endLine" or
"maxHops" or "maxDepth" or "depth" or "parallelism" => "integer",
"excludeTests" or "includeGenerated" or "rawQuery" or "noDedup" or "exactSubstring" or
"exactName" or "exact" or "prefix" or "countOnly" or "includeBody" or "lsp_compatible" or
"regex" or "withPaths" or "rebuild" or "dryRun" or "dry_run" or "force" or "optimize" => "boolean",
"query" or "lang" or "kind" or "format" or "rankBy" or "since" or "path" or "project" or
"solution" or "symbol" or "direction" or "groupBy" or "category" or "language" or
"description" or "context" or "toolInvocationContext" or "db" => "string",
"queries" => "array",
_ => string.Empty,
};

if (expected.Length == 0)
return false;

return true;
}

private static bool MatchesExpectedJsonType(JsonNode? node, string expected) => expected switch
{
"integer" => node is JsonValue value && value.TryGetValue<int>(out _),
"boolean" => node is JsonValue value && value.TryGetValue<bool>(out _),
"string" => node is JsonValue value && value.TryGetValue<string>(out _),
"array" => node is JsonArray,
_ => true,
};

private static string DescribeJsonType(JsonNode? node)
{
if (node is null)
return "null";
return node.GetValueKind() switch
{
JsonValueKind.String => "string",
JsonValueKind.Number => "number",
JsonValueKind.True or JsonValueKind.False => "boolean",
JsonValueKind.Array => "array",
JsonValueKind.Object => "object",
JsonValueKind.Null => "null",
_ => "unknown",
};
}

private static bool IsKnownToolName(string toolName) => toolName switch
{
"search" or "definition" or "references" or "callers" or "callees" or "symbols" or
Expand Down
Loading
Loading