a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
80 lines
4.0 KiB
Go
80 lines
4.0 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/mark3labs/mcp-go/mcp"
|
|
|
|
"github.com/zzet/gortex/internal/llm"
|
|
"github.com/zzet/gortex/internal/llm/conversationlog"
|
|
)
|
|
|
|
// registerLLMTools registers the `ask` MCP tool when an LLM service
|
|
// has been attached via SetLLMService and the service is enabled
|
|
// (model path configured). When either is missing, the tool is
|
|
// silently absent from tools/list — clean degradation for builds /
|
|
// deployments without an LLM.
|
|
func (s *Server) registerLLMTools() {
|
|
if s.llmService == nil || !s.llmService.Enabled() {
|
|
return
|
|
}
|
|
s.addTool(
|
|
mcp.NewTool("ask",
|
|
mcp.WithDescription("Ask a research agent to navigate the gortex graph and return a synthesized answer. The agent runs on whichever LLM provider is configured (`llm.provider`): an in-process llama.cpp model, or a hosted Anthropic / OpenAI / Ollama backend. Use this instead of issuing many search_symbols / get_callers / contracts calls yourself when the question is open-ended or requires multi-hop reasoning across repos — the agent does that work and returns a filtered answer. Set chain=true for cross-system call-chain tracing (consumer → contract → provider → downstream). When `llm.routing` is enabled the agent is dispatched to a cheaper or more capable model by task complexity; the chosen `model` and `complexity` ride on the response."),
|
|
mcp.WithString("question", mcp.Required(), mcp.Description("Natural-language question about the indexed codebase. Examples: \"who calls NewServer in the mcp package?\", \"trace the path from web's /v1/stats consumer to the gortex handler\".")),
|
|
mcp.WithString("repo", mcp.Description("Optional repo-prefix scope (e.g. \"gortex-cloud\"). Restricts the agent's tool calls to one repo. Leave empty for cross-repo questions.")),
|
|
mcp.WithString("project", mcp.Description("Optional project scope.")),
|
|
mcp.WithString("ref", mcp.Description("Optional ref tag scope.")),
|
|
mcp.WithBoolean("chain", mcp.Description("Enable cross-system chain mode: gives the agent the contracts + get_dependencies tools and a chain-tracing prompt. Use when the question is about how a request flows across repos. Default false.")),
|
|
mcp.WithBoolean("include_transcript", mcp.Description("Include the agent's full step-by-step transcript in the response. Useful for debugging the agent's reasoning. Default false (compact response).")),
|
|
),
|
|
s.handleAsk,
|
|
)
|
|
}
|
|
|
|
// handleAsk delegates a single MCP `ask` invocation to the LLM
|
|
// service's RunAgent. The agent's typed AgentAnswer is JSON-marshaled
|
|
// into the MCP text content block — that's the same shape the
|
|
// existing handlers use for structured responses.
|
|
func (s *Server) handleAsk(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
if s.llmService == nil || !s.llmService.Enabled() {
|
|
return mcp.NewToolResultError("llm: service is not configured on this server"), nil
|
|
}
|
|
args := req.GetArguments()
|
|
question, _ := args["question"].(string)
|
|
repo, _ := args["repo"].(string)
|
|
project, _ := args["project"].(string)
|
|
ref, _ := args["ref"].(string)
|
|
chain, _ := boolArg(args, "chain")
|
|
includeTranscript, _ := boolArg(args, "include_transcript")
|
|
|
|
// Label the turn for the conversation-log sink (no-op unless the sink
|
|
// is opted in): the session id + scoped repo + the "ask" phase.
|
|
sessionRepo := repo
|
|
if sessionRepo == "" {
|
|
sessionRepo, _ = s.sessionLocality(ctx)
|
|
}
|
|
ctx = conversationlog.WithMeta(ctx, conversationlog.Meta{
|
|
Session: SessionIDFromContext(ctx),
|
|
Repo: sessionRepo,
|
|
Phase: "ask",
|
|
})
|
|
|
|
answer, err := s.llmService.RunAgent(ctx, llm.RunAgentOptions{
|
|
Question: question,
|
|
Scope: llm.Scope{Repo: repo, Project: project, Ref: ref},
|
|
Chain: chain,
|
|
IncludeTranscript: includeTranscript,
|
|
})
|
|
if err != nil && answer == nil {
|
|
return mcp.NewToolResultError(fmt.Sprintf("llm: %v", err)), nil
|
|
}
|
|
out, mErr := json.MarshalIndent(answer, "", " ")
|
|
if mErr != nil {
|
|
return mcp.NewToolResultError(fmt.Sprintf("llm: marshal answer: %v", mErr)), nil
|
|
}
|
|
return mcp.NewToolResultText(string(out)), nil
|
|
}
|