Files
hexis/internal/mcp/adapter.go
T
kami cca63269e1 Initial commit: Hexis capability registry + execution service baseline
Go daemon (hexisd/hexisctl) implementing capability registry, guarded
execution (confirmations, blessed-entity checks), systemd/workspace-mcp
providers per ECOSYSTEM-SPEC.md. Snapshotting existing working state
before further development.
2026-07-20 00:50:37 +04:00

299 lines
8.1 KiB
Go

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
}