diff --git a/apps/server/src/main/kotlin/com/correx/apps/server/Main.kt b/apps/server/src/main/kotlin/com/correx/apps/server/Main.kt index 9d802d56..8a7b1998 100644 --- a/apps/server/src/main/kotlin/com/correx/apps/server/Main.kt +++ b/apps/server/src/main/kotlin/com/correx/apps/server/Main.kt @@ -296,6 +296,14 @@ fun main() { }) } val mcpTools = mountedMcpServers.flatMap { it.tools } + // Index the workspace into codebase-memory (#242) off the startup path — a fast index makes its + // search tools usable by the first session instead of failing "not indexed"; a slow index just + // means the first stage or two miss it, not a blocked boot. + if (mcpTools.any { it.name.endsWith("__index_repository") }) { + Thread { + runBlocking { bootstrapCodebaseIndex(mcpTools, workspaceRoot, log) } + }.apply { isDaemon = true; name = "mcp-index-bootstrap" }.start() + } val extraTools = taskTools + toolOutputTool + mcpTools val toolRegistry = InfrastructureModule.createToolRegistry( buildToolConfig( diff --git a/apps/server/src/main/kotlin/com/correx/apps/server/McpIndexBootstrap.kt b/apps/server/src/main/kotlin/com/correx/apps/server/McpIndexBootstrap.kt new file mode 100644 index 00000000..dbef3819 --- /dev/null +++ b/apps/server/src/main/kotlin/com/correx/apps/server/McpIndexBootstrap.kt @@ -0,0 +1,40 @@ +package com.correx.apps.server + +import com.correx.core.events.events.ToolRequest +import com.correx.core.events.types.SessionId +import com.correx.core.events.types.StageId +import com.correx.core.events.types.ToolInvocationId +import com.correx.core.tools.contract.Tool +import com.correx.core.tools.contract.ToolExecutor +import com.correx.core.tools.contract.ToolResult +import org.slf4j.Logger +import java.nio.file.Path +import java.util.UUID + +/** + * Bootstraps the codebase-memory index for the boot workspace (#242): its search tools are dead on + * arrival ("project not found or not indexed") until `index_repository` runs once against the repo, + * and nothing in a workflow calls it. If a mounted MCP server advertises an `index_repository` tool + * we invoke it here, at working-dir open, with the workspace root — through the normal ToolExecutor + * path, so no special-casing. + * + * ponytail: single-workspace boot only, "fast" mode. When per-connection working dirs land, hang + * this off the WS-open hook as one entry in a bootstrap registry; one action doesn't earn a registry + * yet. "fast" skips similarity/semantic edges — quickest to ready; upgrade the mode if search recall + * proves thin. + */ +internal suspend fun bootstrapCodebaseIndex(mcpTools: List, workspaceRoot: Path, log: Logger) { + val tool = mcpTools.firstOrNull { it.name.endsWith("__index_repository") } ?: return + val executor = tool as? ToolExecutor ?: return + val request = ToolRequest( + invocationId = ToolInvocationId(UUID.randomUUID().toString()), + sessionId = SessionId("boot"), + stageId = StageId("boot"), + toolName = tool.name, + parameters = mapOf("repo_path" to workspaceRoot.toString(), "mode" to "fast"), + ) + when (val result = executor.execute(request)) { + is ToolResult.Success -> log.info("Indexed workspace into codebase-memory ({})", tool.name) + is ToolResult.Failure -> log.warn("codebase-memory index bootstrap failed: {}", result.reason) + } +} diff --git a/apps/server/src/test/kotlin/com/correx/apps/server/McpIndexBootstrapTest.kt b/apps/server/src/test/kotlin/com/correx/apps/server/McpIndexBootstrapTest.kt new file mode 100644 index 00000000..7268ce7d --- /dev/null +++ b/apps/server/src/test/kotlin/com/correx/apps/server/McpIndexBootstrapTest.kt @@ -0,0 +1,47 @@ +package com.correx.apps.server + +import com.correx.core.approvals.Tier +import com.correx.core.events.events.ToolRequest +import com.correx.core.tools.contract.Tool +import com.correx.core.tools.contract.ToolCapability +import com.correx.core.tools.contract.ToolExecutor +import com.correx.core.tools.contract.ToolResult +import com.correx.core.tools.contract.ValidationResult +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.JsonObject +import org.junit.jupiter.api.Test +import org.slf4j.LoggerFactory +import java.nio.file.Path +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class McpIndexBootstrapTest { + private val log = LoggerFactory.getLogger("test") + + private class FakeTool(override val name: String) : Tool, ToolExecutor { + var seen: ToolRequest? = null + override val description = "" + override val parametersSchema = JsonObject(emptyMap()) + override val tier = Tier.T2 + override val requiredCapabilities = emptySet() + override fun validateRequest(request: ToolRequest) = ValidationResult.Valid + override suspend fun execute(request: ToolRequest): ToolResult { + seen = request + return ToolResult.Success(request.invocationId, "indexed") + } + } + + @Test + fun `calls index_repository with repo_path set to the workspace root`() = runBlocking { + val index = FakeTool("mcp__codebase-memory__index_repository") + bootstrapCodebaseIndex(listOf(FakeTool("mcp__x__search_code"), index), Path.of("/repo"), log) + assertEquals("/repo", index.seen?.parameters?.get("repo_path")) + } + + @Test + fun `no-op when no index_repository tool is mounted`() = runBlocking { + val other = FakeTool("mcp__x__search_code") + bootstrapCodebaseIndex(listOf(other), Path.of("/repo"), log) + assertNull(other.seen) + } +}