feat(server): bootstrap codebase-memory index at workspace open (#242)
codebase-memory search tools failed 9/9 ("project not found or not indexed")
because index_repository was never called for the repo. After MCP servers
mount, if one advertises index_repository, invoke it once with the workspace
root (repo_path) through the normal ToolExecutor path, on a background daemon
so a slow index doesn't block startup. "fast" mode to reach usable quickest.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -296,6 +296,14 @@ fun main() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
val mcpTools = mountedMcpServers.flatMap { it.tools }
|
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 extraTools = taskTools + toolOutputTool + mcpTools
|
||||||
val toolRegistry = InfrastructureModule.createToolRegistry(
|
val toolRegistry = InfrastructureModule.createToolRegistry(
|
||||||
buildToolConfig(
|
buildToolConfig(
|
||||||
|
|||||||
@@ -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<Tool>, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ToolCapability>()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user