// Package mcp exposes asyncmachines over mcp-go as an MCP server. // See /tools/debugger/misc_dbg.(newMcpServer) for an example.
package mcp // TODO add test &/ example import ( amhelp am ) type Opts struct { StatesInclude am.S StatesExclude am.S StatesReadonly am.S // list of typed args Args []am.ArgsApi // RPC args parser ArgsUnmarshaller amhelp.ArgsUnmarshallerFn CallSignatures []am.CallSignature // optional func to call after a mutation MutCallback func(ctx context.Context) error Version string Name string Desc string StateCalls []am.CallSignature } type Server struct { Mach am.Api Mcp *server.MCPServer Http *server.StreamableHTTPServer Opts Opts // all the names of type safe args argNames []string } func ( am.Api, Opts) (*Server, error) { var error // validate if .Args == nil { return nil, fmt.Errorf("field ArgsBase required") } if .Name == "" { return nil, fmt.Errorf("field Name required") } if .Version == "" { return nil, fmt.Errorf("field Version required") } if .ArgsUnmarshaller == nil { .ArgsUnmarshaller = amhelp.NewArgsUnmarshaller(.Args) } // Create a new MCP server := server.NewMCPServer(.Name, .Version, server.WithToolCapabilities(false), server.WithDescription(.Desc), ) := &Server{ Mach: , Mcp: , Opts: , } // MACHINE // collect arg names .argNames, = amhelp.ArgsNames(.Args) if != nil { return nil, } var []mcp.ToolOption for , := range .argNames { = append(, mcp.WithString()) } // Add := []mcp.ToolOption{ mcp.WithDescription("Add a state with optional params"), mcp.WithString("state", mcp.Required(), mcp.Description("Name of the state to add"), mcp.Enum(.StateNames()...), ), } = append(, ...) := mcp.NewTool("Add", ...) // Remove = []mcp.ToolOption{ mcp.WithDescription("Remove a state with optional params"), mcp.WithString("state", mcp.Required(), mcp.Description("Name of the state to remove"), // TODO allowlist mcp.Enum(.StateNames()...), ), } = append(, ...) := mcp.NewTool("Remove", ...) // specified call signatures : for , := range .StateCalls { // perms for , := range .States { if !.stateMutable() { continue } } , := .newCallSigHandler(&) .AddTool(, ) } // TODO Serialize, Inspect, Times (sum, queueTick, machTick) // bind handlers .AddTool(, .mutAdd) .AddTool(, .mutRemove) .Http = server.NewStreamableHTTPServer() return , nil } func ( *Server) ( context.Context, mcp.CallToolRequest, ) (*mcp.CallToolResult, error) { , := .RequireString("state") if != nil { return mcp.NewToolResultError(.Error()), nil } // perms if !.stateMutable() { return mcp.NewToolResultError(fmt.Sprintf( "state %s not allowed", )), nil } // parse args := am.A{"FromMCP": true} for , := range .argNames { if := .GetString(, ""); != "" { [] = } } // mutate := .Mach.Add1(, .Opts.ArgsUnmarshaller()) // wait until processed if == am.Canceled { return mcp.NewToolResultError("canceled"), nil } select { case <-.Mach.WhenQueue(): case <-.Done(): return nil, .Err() } // callback if .Opts.MutCallback != nil { := .Opts.MutCallback() if != nil { return mcp.NewToolResultError(fmt.Sprintf( "callback error: %s", )), nil } } return .response() } func ( *Server) ( context.Context, mcp.CallToolRequest, ) (*mcp.CallToolResult, error) { , := .RequireString("state") if != nil { return mcp.NewToolResultError(.Error()), nil } // perms if !.stateMutable() { return mcp.NewToolResultError(fmt.Sprintf( "state %s not allowed", )), nil } // parse args := am.A{"FromMCP": true} for , := range .argNames { [] = .GetString(, "") } // mutate := .Mach.Add1(, nil) // wait until processed if == am.Canceled { return mcp.NewToolResultError("canceled"), nil } select { case <-.Mach.WhenQueue(): case <-.Done(): return nil, .Err() } // callback if .Opts.MutCallback != nil { := .Opts.MutCallback() if != nil { return mcp.NewToolResultError(fmt.Sprintf( "callback error: %s", )), nil } } return .response() } func ( *Server) ( *am.CallSignature, ) (mcp.Tool, server.ToolHandlerFunc) { := .Name if == "" { = .String() } // collect opts var []mcp.ToolOption if .Desc != "" { = append(, mcp.WithDescription(.Desc)) } // collect args for , := range .Args() { var []mcp.PropertyOption if , := .Values[]; { = append(, mcp.Enum(...)) } if slices.Contains(.Needed, ) { = append(, mcp.Required()) } = append(, mcp.WithString(, ...)) } // tool & handler := mcp.NewTool(, ...) return , func( context.Context, mcp.CallToolRequest, ) (*mcp.CallToolResult, error) { // parse args := am.A{"FromMCP": true} for , := range .Args() { [] = .GetString(, "") } // mutate var am.Result if .IsRemove { = .Mach.Remove(.States, .Opts.ArgsUnmarshaller()) } else { = .Mach.Add(.States, .Opts.ArgsUnmarshaller()) } // wait until processed if == am.Canceled { return mcp.NewToolResultError("canceled"), nil } select { case <-.Mach.WhenQueue(): case <-.Done(): return nil, .Err() } // callback if .Opts.MutCallback != nil { := .Opts.MutCallback() if != nil { return mcp.NewToolResultError(fmt.Sprintf( "callback error: %s", )), nil } } return .response() } } // response is a generic response with clocks of active states func ( *Server) () (*mcp.CallToolResult, error) { := "(" for , := range .Mach.Clock(nil) { if !.stateMutable() && !slices.Contains(.Opts.StatesReadonly, ) { continue } if !am.IsActiveTick() { continue } if != "(" { += " " } += fmt.Sprintf("%s:%d", , ) } return mcp.NewToolResultText( + ")"), nil } func ( *Server) ( string) bool { if len(.Opts.StatesInclude) > 0 { return slices.Contains(.Opts.StatesInclude, ) } return !slices.Contains(.Opts.StatesExclude, ) } func ( *Server) () am.S { return slices.DeleteFunc(slices.Clone(.Mach.StateNames()), func( string) bool { return !.stateMutable() }) }