Files

157 lines
4.3 KiB
Go

package mcp_impl
import (
"context"
"fmt"
"strings"
"github.com/hanzo-fi/numscript/internal/analysis"
"github.com/hanzo-fi/numscript/internal/interpreter"
"github.com/hanzo-fi/numscript/internal/parser"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
func addEvalTool(s *server.MCPServer) {
tool := mcp.NewTool("evaluate",
mcp.WithDescription("Evaluate a numscript program"),
mcp.WithIdempotentHintAnnotation(true),
mcp.WithReadOnlyHintAnnotation(true),
mcp.WithOpenWorldHintAnnotation(false),
mcp.WithString("script",
mcp.Required(),
mcp.Description("The numscript source"),
),
mcp.WithArray("balances",
mcp.Required(),
mcp.Description(`The accounts' balances. A list of entries, each an object with an "account", an "asset", an integer "amount", an optional "color", and an optional "scope".
The (account, asset, color, scope) tuple of each entry must be unique within the list.
For example: [ { "account": "alice", "asset": "USD/2", "amount": 100 }, { "account": "alice", "asset": "EUR/2", "amount": -42 }, { "account": "bob", "asset": "BTC", "amount": 1 } ]
`),
),
mcp.WithObject("vars",
mcp.Required(),
mcp.Description(`The stringified variables to be passed to the script's "vars" block.
For example: { "acc": "alice", "mon": "EUR 100" } can be passed to the following script:
vars {
monetary $mon
account $acc
}
send $mon (
source = $acc
destination = @world
)
`),
),
)
s.AddTool(tool, handleEvalTool)
}
func handleEvalTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
script, err := request.RequireString("script")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
parsed := parser.Parse(script)
if len(parsed.Errors) != 0 {
out := make([]string, len(parsed.Errors))
for index, err := range parsed.Errors {
out[index] = err.Msg
}
return mcp.NewToolResultError(strings.Join(out, ", ")), nil
}
var args struct {
Vars map[string]string `json:"vars"`
Balances interpreter.Balances `json:"balances"`
}
err = request.BindArguments(&args)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
if dup, ok := args.Balances.FirstDuplicate(); ok {
key := fmt.Sprintf("account=%q asset=%q", dup.Account, dup.Asset)
if dup.Color != "" {
key += fmt.Sprintf(" color=%q", dup.Color)
}
if dup.Scope != "" {
key += fmt.Sprintf(" scope=%q", dup.Scope)
}
return mcp.NewToolResultError("balances must not contain duplicate entries: duplicate entry for " + key), nil
}
out, iErr := interpreter.RunProgram(
ctx,
parsed.Value,
args.Vars,
interpreter.StaticStore{
Balances: args.Balances,
},
map[string]struct{}{},
)
if iErr != nil {
return mcp.NewToolResultError(iErr.Error()), nil
}
return mcp.NewToolResultJSON(*out)
}
func addCheckTool(s *server.MCPServer) {
tool := mcp.NewTool("check",
mcp.WithDescription("Check a program for parsing error or static analysis errors"),
mcp.WithIdempotentHintAnnotation(true),
mcp.WithReadOnlyHintAnnotation(true),
mcp.WithOpenWorldHintAnnotation(false),
mcp.WithString("script",
mcp.Required(),
mcp.Description("The numscript source"),
),
)
s.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
script, err := request.RequireString("script")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
checkResult := analysis.CheckSource(script)
var errors []any
for _, d := range checkResult.Diagnostics {
errors = append(errors, map[string]any{
"kind": d.Kind.Message(),
"severity": analysis.SeverityToString(d.Kind.Severity()),
"span": d.Range,
})
}
return mcp.NewToolResultJSON(map[string]any{
"errors": errors,
})
})
}
func RunServer() error {
// Create a new MCP server
s := server.NewMCPServer(
"Numscript",
"0.0.1",
server.WithToolCapabilities(false),
server.WithRecovery(),
server.WithInstructions(`
You're a Numscript expert AI assistant. Numscript is a DSL that allows modeling financial transactions in an easy and declarative way. Numscript scripts always terminate.
`),
)
addEvalTool(s)
addCheckTool(s)
// Start the server
if err := server.ServeStdio(s); err != nil {
return err
}
return nil
}