diff --git a/plugins/mcp-server/src/com/intellij/mcpserver/McpToolsMarkdownExporter.kt b/plugins/mcp-server/src/com/intellij/mcpserver/McpToolsMarkdownExporter.kt
index e0889d124b47..ec8efe3d680a 100644
--- a/plugins/mcp-server/src/com/intellij/mcpserver/McpToolsMarkdownExporter.kt
+++ b/plugins/mcp-server/src/com/intellij/mcpserver/McpToolsMarkdownExporter.kt
@@ -22,20 +22,22 @@ object McpToolsMarkdownExporter {
appendLine("## ${category.shortName}")
appendLine()
for (tool in categoryTools) {
- appendToolSection(tool, headingLevel = 3)
+ appendToolSection(tool, headingLevel = 3, omitFirstDescriptionLine = false)
}
}
}
}
/**
- * Generates a single-page markdown document for one MCP tool: top-level heading, full description,
+ * Generates a single-page markdown document for one MCP tool: top-level heading, description,
* input parameters table, and output schema table (when present).
*
- * Use together with [generateMarkdownTree] when emitting per-tool reference files.
+ * [omitFirstDescriptionLine] drops the first non-blank line of the description while keeping the rest.
+ * The per-tool files of [generateMarkdownTree] pass `true`: that first line is already shown as the
+ * tool's summary in the index, so repeating it in the per-tool file is duplication and wasted context.
*/
- fun generateMarkdownForTool(tool: McpTool): String = buildString {
- appendToolSection(tool, headingLevel = 1)
+ fun generateMarkdownForTool(tool: McpTool, omitFirstDescriptionLine: Boolean = false): String = buildString {
+ appendToolSection(tool, headingLevel = 1, omitFirstDescriptionLine = omitFirstDescriptionLine)
}
/**
@@ -71,7 +73,7 @@ object McpToolsMarkdownExporter {
}
for ((_, categoryTools) in byCategory) {
for (tool in categoryTools) {
- result["$TREE_TOOLS_SUBDIR/${tool.descriptor.name}.md"] = generateMarkdownForTool(tool)
+ result["$TREE_TOOLS_SUBDIR/${tool.descriptor.name}.md"] = generateMarkdownForTool(tool, omitFirstDescriptionLine = true)
}
}
return result
@@ -99,10 +101,15 @@ object McpToolsMarkdownExporter {
"Nested `*` is scoped to the parent object's presence — required nested fields are only guaranteed " +
"when the optional parent object is emitted."
- private fun StringBuilder.appendToolSection(tool: McpTool, headingLevel: Int) {
+ private fun StringBuilder.appendToolSection(tool: McpTool, headingLevel: Int, omitFirstDescriptionLine: Boolean) {
val heading = "#".repeat(headingLevel)
appendLine("$heading ${tool.descriptor.name}")
- appendLine(tool.descriptor.description.trimIndent().escapeLineBreaks().escapeMarkdown())
+ val description =
+ if (omitFirstDescriptionLine) tool.descriptor.description.dropFirstNonBlankLine()
+ else tool.descriptor.description.trimIndent()
+ if (description.isNotBlank()) {
+ appendLine(description.escapeLineBreaks().escapeMarkdown())
+ }
appendLine()
appendLine("$heading# Parameters")
val inputRows = tool.descriptor.inputSchema.toSchemaTableRows()
@@ -243,7 +250,20 @@ object McpToolsMarkdownExporter {
return required.mapNotNull { (it as? JsonPrimitive)?.content }.toSet()
}
- private fun String.escapeLineBreaks(): String =
+ /**
+ * Drops the first non-blank line (and any blank lines preceding it) and returns the remaining body,
+ * trimmed. That first line is the tool summary shown in the index ([generateMarkdownTree]); see
+ * [generateMarkdownForTool]'s `omitFirstDescriptionLine`. Returns an empty string for a single-line
+ * description.
+ */
+ private fun String.dropFirstNonBlankLine(): String {
+ val lines = trim().lines()
+ val firstNonBlank = lines.indexOfFirst { it.isNotBlank() }
+ if (firstNonBlank < 0) return ""
+ return lines.drop(firstNonBlank + 1).joinToString("\n").trim()
+ }
+
+ private fun String.escapeLineBreaks(): String =
replace("\r\n", "
").replace("\n", "
").replace("\r", "
")
private fun String.escapeMarkdown(): String = replace("|", "\\|")
diff --git a/plugins/mcp-server/src/com/intellij/mcpserver/toolsets/general/ExecutionToolset.kt b/plugins/mcp-server/src/com/intellij/mcpserver/toolsets/general/ExecutionToolset.kt
index 6e5e341b135d..3bd40734e801 100644
--- a/plugins/mcp-server/src/com/intellij/mcpserver/toolsets/general/ExecutionToolset.kt
+++ b/plugins/mcp-server/src/com/intellij/mcpserver/toolsets/general/ExecutionToolset.kt
@@ -97,8 +97,8 @@ class ExecutionToolset : McpToolset {
@McpTool(name = ExecutionToolNames.EXECUTE_RUN_CONFIGURATION)
@McpDescription("""
- |Run either an existing run configuration by name or a temporary run configuration created from a code location
- |(`filePath` + `line`) in the current project, then wait up to specified timeout for it to finish.
+ |Run either an existing run configuration by name or a temporary run configuration created from a code location (`filePath` + `line`) in the current project, then wait up to specified timeout for it to finish.
+ |
|Use this tool with either a configuration name returned by `get_run_configurations`, or with a run point
|(`filePath` + `line`) returned by `get_run_configurations(filePath = ...)`.
|
diff --git a/plugins/mcp-server/tests/testSrc/com/intellij/mcpserver/McpToolsMarkdownExporterTest.kt b/plugins/mcp-server/tests/testSrc/com/intellij/mcpserver/McpToolsMarkdownExporterTest.kt
index caef8ddbba6f..c532bee168c7 100644
--- a/plugins/mcp-server/tests/testSrc/com/intellij/mcpserver/McpToolsMarkdownExporterTest.kt
+++ b/plugins/mcp-server/tests/testSrc/com/intellij/mcpserver/McpToolsMarkdownExporterTest.kt
@@ -296,16 +296,37 @@ class McpToolsMarkdownExporterTest {
}
@Test
- fun generateMarkdownTreePerToolFileMatchesGenerateMarkdownForToolOutput() {
+ fun generateMarkdownTreePerToolFileMatchesFirstLineOmittedSingleToolOutput() {
val t = setMarkerTool()
val result = McpToolsMarkdownExporter.generateMarkdownTree(listOf(t))
assertEquals(
- McpToolsMarkdownExporter.generateMarkdownForTool(t),
+ McpToolsMarkdownExporter.generateMarkdownForTool(t, omitFirstDescriptionLine = true),
result.getValue("$TREE_TOOLS_SUBDIR/set_marker.md"),
)
}
+ @Test
+ fun generateMarkdownTreePerToolFileDropsFirstDescriptionLineButKeepsBody() {
+ val t = startTaskTool(
+ description = "Starts a task using the supplied configuration.\nReturns the task handle.",
+ )
+
+ val result = McpToolsMarkdownExporter.generateMarkdownTree(listOf(t))
+ val index = result.getValue(TREE_INDEX_FILE)
+ val perTool = result.getValue("$TREE_TOOLS_SUBDIR/start_task.md")
+
+ assert(index.contains("Starts a task using the supplied configuration.")) {
+ "Index should keep the first description line: $index"
+ }
+ assert(!perTool.contains("Starts a task using the supplied configuration.")) {
+ "Per-tool file should drop the first description line (it lives in the index): $perTool"
+ }
+ assert(perTool.contains("Returns the task handle.")) {
+ "Per-tool file should keep the rest of the description body: $perTool"
+ }
+ }
+
@Test
fun generateMarkdownTreeEmptyListReturnsOnlyIndexHeaderAndLegend() {
val result = McpToolsMarkdownExporter.generateMarkdownTree(emptyList())