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
115 lines
4.6 KiB
Go
115 lines
4.6 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/mark3labs/mcp-go/mcp"
|
|
)
|
|
|
|
// These are unconditionally registered (single-project mode degrades
|
|
// to a one-member view) so an agent's first call into the server can
|
|
// discover what `repo` values are legal before issuing any
|
|
// scope: repo or scope: fan-out call.
|
|
func (s *Server) registerWorkspaceTools() {
|
|
s.addTool(
|
|
mcp.NewTool("list_repos",
|
|
mcp.WithDescription(
|
|
"Lists every project in the active workspace. Workspace-scope tool: do not pass `repo`. "+
|
|
"In workspace mode returns the auto-discovered, non-excluded children. "+
|
|
"In single-project mode returns the one bound project as a degenerate one-member workspace."),
|
|
mcp.WithString("format", mcp.Description("Output format: json (default), gcx (GCX1 compact wire format), or toon")),
|
|
mcp.WithNumber("max_bytes", mcp.Description("Cap the marshaled response at this many bytes; truncation metadata rides on the response.")),
|
|
),
|
|
s.handleListRepos,
|
|
)
|
|
|
|
s.addTool(
|
|
mcp.NewTool("workspace_info",
|
|
mcp.WithDescription(
|
|
"Returns workspace identity: bind mode, root directory, marker contents, the auto-discovered member set, and any unknown marker keys. "+
|
|
"Workspace-scope tool: do not pass `repo`."),
|
|
mcp.WithString("format", mcp.Description("Output format: json (default), gcx (GCX1 compact wire format), or toon")),
|
|
mcp.WithNumber("max_bytes", mcp.Description("Cap the marshaled response at this many bytes; truncation metadata rides on the response.")),
|
|
),
|
|
s.handleWorkspaceInfo,
|
|
)
|
|
}
|
|
|
|
// handleListRepos implements scope: workspace's `list_repos`. Returns
|
|
// the auto-discovered, non-excluded member set. Single-project mode
|
|
// degrades to the one-member [bound project] list.
|
|
//
|
|
// Pre-handshake (Bind() == nil): returns an empty list rather than
|
|
// erroring. The MCP server may be running in legacy single-repo mode
|
|
// where no two-entry-point handshake has happened — in that case the
|
|
// concept of a workspace doesn't apply and an empty list is the
|
|
// honest answer.
|
|
func (s *Server) handleListRepos(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
// Enforce scope: workspace's "no `repo`" rule explicitly so a
|
|
// caller passing `repo` gets a clear protocol error instead of
|
|
// silent acceptance.
|
|
if _, errResult := s.ResolveToolScope("list_repos", req.GetArguments()["repo"]); errResult != nil {
|
|
return errResult, nil
|
|
}
|
|
|
|
return s.respondJSONOrTOON(ctx, req, s.buildListReposPayload(ctx))
|
|
}
|
|
|
|
// buildListReposPayload returns the same data the `list_repos` tool
|
|
// emits. Shared with the `gortex://repos` resource.
|
|
//
|
|
// A workspace-bound session (daemon socket path) reports the repos in
|
|
// its own resolved workspace; an unbound session falls back to the
|
|
// process-global bind.
|
|
func (s *Server) buildListReposPayload(ctx context.Context) map[string]any {
|
|
if sessWS, _, bound := s.sessionScope(ctx); bound {
|
|
repos := s.sessionWorkspaceRepos(ctx)
|
|
names := make([]string, 0, len(repos))
|
|
for _, r := range repos {
|
|
names = append(names, r["name"])
|
|
}
|
|
return map[string]any{
|
|
"mode": "workspace",
|
|
"workspace": sessWS,
|
|
"repos": names,
|
|
}
|
|
}
|
|
// No session workspace (embedded single-repo server, or a control
|
|
// client): there is no workspace boundary to report.
|
|
return map[string]any{"mode": "unbound", "repos": []string{}}
|
|
}
|
|
|
|
// handleWorkspaceInfo implements `workspace_info`. Returns enough
|
|
// detail for an agent to reason about the bind: mode, root, marker
|
|
// excludes, marker unknown keys, and the resolved member set with
|
|
// per-member paths.
|
|
func (s *Server) handleWorkspaceInfo(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
if _, errResult := s.ResolveToolScope("workspace_info", req.GetArguments()["repo"]); errResult != nil {
|
|
return errResult, nil
|
|
}
|
|
return s.respondJSONOrTOON(ctx, req, s.buildWorkspaceInfoPayload(ctx))
|
|
}
|
|
|
|
// buildWorkspaceInfoPayload returns the same data the `workspace_info`
|
|
// tool emits. Shared with the `gortex://workspace` resource.
|
|
//
|
|
// A workspace-bound session (daemon socket path) reports its own
|
|
// resolved workspace — the boundary the query tools enforce — instead
|
|
// of the process-global bind, which is nil on the daemon.
|
|
func (s *Server) buildWorkspaceInfoPayload(ctx context.Context) map[string]any {
|
|
if sessWS, sessProj, bound := s.sessionScope(ctx); bound {
|
|
return map[string]any{
|
|
"mode": "workspace",
|
|
"workspace": sessWS,
|
|
"project": sessProj,
|
|
"members": s.sessionWorkspaceRepos(ctx),
|
|
"isolation_bounds": sessWS,
|
|
}
|
|
}
|
|
// No session workspace: embedded single-repo / control client.
|
|
return map[string]any{
|
|
"mode": "unbound",
|
|
"repos": []map[string]string{},
|
|
}
|
|
}
|