cca63269e1
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.
299 lines
8.1 KiB
Go
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
|
|
}
|