package mcp import ( "context" "encoding/json" "fmt" "strconv" "strings" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "github.com/kami/hexis/internal/domain" "github.com/kami/hexis/internal/execution" "github.com/kami/hexis/internal/nexusclient" "github.com/kami/hexis/internal/storage" "github.com/kami/hexis/internal/wire" ) type Adapter struct { server *server.MCPServer store storage.Interface engine *execution.Engine nexus nexusclient.Client } func New(store storage.Interface, engine *execution.Engine, nexus nexusclient.Client) *Adapter { a := &Adapter{ store: store, engine: engine, nexus: nexus, } mcpServer := server.NewMCPServer( "hexis", "1.0.0", server.WithResourceCapabilities(true, true), server.WithToolCapabilities(true), ) mcpServer.AddTool(mcp.NewTool("hexis.list_capabilities", mcp.WithDescription("List capabilities, optionally filtered by entity_id"), mcp.WithString("entity_id", mcp.Description("Optional entity ID to filter capabilities"), ), ), a.handleListCapabilities) mcpServer.AddTool(mcp.NewTool("hexis.inspect_capability", mcp.WithDescription("Get capability details by ID"), mcp.WithString("capability_id", mcp.Description("Capability ID"), mcp.Required(), ), ), a.handleInspectCapability) mcpServer.AddTool(mcp.NewTool("hexis.resolve_target", mcp.WithDescription("Resolve a free-text target to a canonical entity ID via Nexus"), mcp.WithString("query", mcp.Description("Free-text query (name, alias, path)"), mcp.Required(), ), mcp.WithString("capability", mcp.Description("Capability name to filter target types"), ), ), a.handleResolveTarget) mcpServer.AddTool(mcp.NewTool("hexis.execute", mcp.WithDescription("Execute a capability against a target entity"), mcp.WithString("capability_id", mcp.Description("Capability ID"), mcp.Required(), ), mcp.WithString("target_entity_id", mcp.Description("Canonical entity ID of the target"), mcp.Required(), ), mcp.WithString("arguments", mcp.Description("JSON string of execution arguments"), ), mcp.WithString("idempotency_key", mcp.Description("Idempotency key for safe retry"), ), mcp.WithString("entity_version", mcp.Description("Expected Nexus entity version, for optimistic concurrency"), ), mcp.WithString("requested_by", mcp.Description("JSON string describing the requester (e.g. {\"type\":\"user\",\"id\":\"...\"})"), ), mcp.WithString("origin", mcp.Description("JSON string describing the request origin (e.g. {\"system\":\"maven\",\"channel\":\"voice\"})"), ), mcp.WithString("correlation_id", mcp.Description("Correlation ID for cross-system tracing"), ), mcp.WithString("causation_id", mcp.Description("Causation ID of the event/request that triggered this execute"), ), mcp.WithString("resolution_evidence", mcp.Description("JSON array of evidence objects backing target_entity_id's resolution"), ), mcp.WithString("confirmation_id", mcp.Description("Confirmation ID for capabilities that require confirmation"), ), ), a.handleExecute) mcpServer.AddTool(mcp.NewTool("hexis.execution_status", mcp.WithDescription("Get execution status by ID"), mcp.WithString("execution_id", mcp.Description("Execution ID"), mcp.Required(), ), ), a.handleExecutionStatus) mcpServer.AddResource(mcp.NewResource("hexis://capabilities", "All capabilities", mcp.WithMIMEType("application/json"), ), a.handleCapabilitiesResource) mcpServer.AddResourceTemplate( mcp.NewResourceTemplate("hexis://capabilities/{id}", "Capability by ID"), a.handleCapabilityResourceTemplate, ) mcpServer.AddResourceTemplate( mcp.NewResourceTemplate("hexis://executions/{id}", "Execution by ID"), a.handleExecutionResourceTemplate, ) a.server = mcpServer return a } func (a *Adapter) ServeStdio() error { return server.ServeStdio(a.server) } func (a *Adapter) MCPServer() *server.MCPServer { return a.server } func (a *Adapter) handleListCapabilities(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { entityID := req.GetString("entity_id", "") caps, err := a.store.ListCapabilities(entityID) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("list capabilities: %v", err)), nil } // Same serializer as the HTTP API, so an MCP client and an HTTP client see // an identical capability — including `enabled` and // `requires_confirmation`, which this tool previously hid. Tool // availability is not permission (spec §4.5), but a client that can see // the guard fields can at least explain a 403 instead of guessing. data, _ := json.MarshalIndent(wire.Capabilities(caps), "", " ") return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{Type: "text", Text: string(data)}, }, }, nil } func (a *Adapter) handleInspectCapability(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { capID := req.GetString("capability_id", "") if capID == "" { return mcp.NewToolResultError("capability_id is required"), nil } cap, err := a.store.GetCapability(capID) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("capability not found: %v", err)), nil } data, _ := json.MarshalIndent(wire.Capability(cap), "", " ") return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{Type: "text", Text: string(data)}, }, }, nil } func (a *Adapter) handleResolveTarget(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { query := req.GetString("query", "") if query == "" { return mcp.NewToolResultError("query is required"), nil } if a.nexus == nil { return mcp.NewToolResultError("resolve_target unavailable: no Nexus URL configured"), nil } var types []string if capName := req.GetString("capability", ""); capName != "" { if caps, err := a.store.ListCapabilities(""); err == nil { for _, c := range caps { if c.Name == capName { types = c.TargetTypes break } } } } result, err := a.nexus.Resolve(ctx, query, types) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("nexus resolve: %v", err)), nil } data, _ := json.MarshalIndent(result, "", " ") return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{Type: "text", Text: string(data)}, }, }, nil } func (a *Adapter) handleExecute(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { capID := req.GetString("capability_id", "") targetID := req.GetString("target_entity_id", "") argsStr := req.GetString("arguments", "") idempKey := req.GetString("idempotency_key", "") if capID == "" || targetID == "" { return mcp.NewToolResultError("capability_id and target_entity_id are required"), nil } args := map[string]any{} if argsStr != "" { json.Unmarshal([]byte(argsStr), &args) } var entityVersion int64 if v := req.GetString("entity_version", ""); v != "" { parsed, err := strconv.ParseInt(v, 10, 64) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("entity_version must be an integer: %v", err)), nil } entityVersion = parsed } var requestedBy map[string]string if v := req.GetString("requested_by", ""); v != "" { if err := json.Unmarshal([]byte(v), &requestedBy); err != nil { return mcp.NewToolResultError(fmt.Sprintf("requested_by must be a JSON object: %v", err)), nil } } var origin map[string]string if v := req.GetString("origin", ""); v != "" { if err := json.Unmarshal([]byte(v), &origin); err != nil { return mcp.NewToolResultError(fmt.Sprintf("origin must be a JSON object: %v", err)), nil } } var resolutionEvidence []map[string]any if v := req.GetString("resolution_evidence", ""); v != "" { if err := json.Unmarshal([]byte(v), &resolutionEvidence); err != nil { return mcp.NewToolResultError(fmt.Sprintf("resolution_evidence must be a JSON array: %v", err)), nil } } execReq := &domain.ExecuteRequest{ CapabilityID: capID, TargetEntityID: targetID, EntityVersion: entityVersion, Arguments: args, RequestedBy: requestedBy, Origin: origin, IdempotencyKey: idempKey, CorrelationID: req.GetString("correlation_id", ""), CausationID: req.GetString("causation_id", ""), ResolutionEvidence: resolutionEvidence, ConfirmationID: req.GetString("confirmation_id", ""), } result, err := a.engine.Execute(execReq) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("execution failed: %v", err)), nil } data, _ := json.MarshalIndent(result.Execution, "", " ") return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{Type: "text", Text: string(data)}, }, }, nil } func (a *Adapter) handleExecutionStatus(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { execID := req.GetString("execution_id", "") if execID == "" { return mcp.NewToolResultError("execution_id is required"), nil } exec, err := a.store.GetExecution(execID) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("execution not found: %v", err)), nil } data, _ := json.MarshalIndent(exec, "", " ") return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{Type: "text", Text: string(data)}, }, }, nil } func (a *Adapter) handleCapabilitiesResource(ctx context.Context, req mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { caps, err := a.store.ListCapabilities("") if err != nil { return nil, err } data, _ := json.MarshalIndent(wire.Capabilities(caps), "", " ") return []mcp.ResourceContents{ mcp.TextResourceContents{ URI: "hexis://capabilities", MIMEType: "application/json", Text: string(data), }, }, nil } func (a *Adapter) handleCapabilityResourceTemplate(ctx context.Context, req mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { uri := req.Params.URI id := strings.TrimPrefix(uri, "hexis://capabilities/") if id == "" { return nil, fmt.Errorf("invalid capability URI: %s", uri) } cap, err := a.store.GetCapability(id) if err != nil { return nil, fmt.Errorf("capability %s: %w", id, err) } data, _ := json.MarshalIndent(wire.Capability(cap), "", " ") return []mcp.ResourceContents{ mcp.TextResourceContents{ URI: uri, MIMEType: "application/json", Text: string(data), }, }, nil } func (a *Adapter) handleExecutionResourceTemplate(ctx context.Context, req mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { uri := req.Params.URI id := strings.TrimPrefix(uri, "hexis://executions/") if id == "" { return nil, fmt.Errorf("invalid execution URI: %s", uri) } exec, err := a.store.GetExecution(id) if err != nil { return nil, fmt.Errorf("execution %s: %w", id, err) } data, _ := json.MarshalIndent(exec, "", " ") return []mcp.ResourceContents{ mcp.TextResourceContents{ URI: uri, MIMEType: "application/json", Text: string(data), }, }, nil }