package mcp import ( "context" "encoding/json" "fmt" "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/storage" ) type Adapter struct { server *server.MCPServer store storage.Interface engine *execution.Engine } func New(store storage.Interface, engine *execution.Engine) *Adapter { a := &Adapter{ store: store, engine: engine, } 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"), ), ), 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 } var result []map[string]any for _, c := range caps { result = append(result, map[string]any{ "id": c.ID, "name": c.Name, "description": c.Description, "target_types": c.TargetTypes, "target_entity_id": c.TargetEntityID, "provider": c.Provider, "operation": c.Operation, "risk": c.Risk, "read_only": c.ReadOnly, }) } data, _ := json.MarshalIndent(result, "", " ") 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(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 } // In a real setup, this would call Nexus API. // For now, return a placeholder indicating Nexus resolution is needed. result := map[string]any{ "query": query, "status": "requires_nexus_resolution", "message": "Connect to Nexus to resolve this query to a canonical entity ID", } 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) } execReq := &domain.ExecuteRequest{ CapabilityID: capID, TargetEntityID: targetID, Arguments: args, IdempotencyKey: idempKey, } 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(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(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 }