From 78f3a7e228aa9f4b3e0ac6082c85f13b30d4ab19 Mon Sep 17 00:00:00 2001 From: ascandone Date: Thu, 2 Oct 2025 22:58:36 +0200 Subject: [PATCH 01/10] draft --- go.mod | 9 +++ go.sum | 21 ++++++ internal/cmd/mcp.go | 22 +++++++ internal/cmd/root.go | 1 + internal/mcp_impl/handlers.go | 118 ++++++++++++++++++++++++++++++++++ 5 files changed, 171 insertions(+) create mode 100644 internal/cmd/mcp.go create mode 100644 internal/mcp_impl/handlers.go diff --git a/go.mod b/go.mod index 4a2a029c..7502df26 100644 --- a/go.mod +++ b/go.mod @@ -6,29 +6,38 @@ require ( github.com/antlr4-go/antlr/v4 v4.13.1 github.com/getsentry/sentry-go v0.35.1 github.com/gkampitakis/go-snaps v0.5.14 + github.com/mark3labs/mcp-go v0.41.1 github.com/sergi/go-diff v1.4.0 github.com/spf13/cobra v1.10.1 github.com/stretchr/testify v1.11.1 ) require ( + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/gkampitakis/ciinfo v0.3.3 // indirect github.com/gkampitakis/go-diff v1.3.2 // indirect github.com/goccy/go-yaml v1.18.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/invopop/jsonschema v0.13.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/maruel/natural v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/segmentio/asm v1.1.3 // indirect github.com/segmentio/encoding v0.3.4 // indirect + github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.2.0 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.lsp.dev/jsonrpc2 v0.10.0 // indirect go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 // indirect go.lsp.dev/protocol v0.12.0 diff --git a/go.sum b/go.sum index 52a1a618..820a5426 100644 --- a/go.sum +++ b/go.sum @@ -2,11 +2,17 @@ github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYW github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/getsentry/sentry-go v0.35.1 h1:iopow6UVLE2aXu46xKVIs8Z9D/YZkJrHkgozrxa+tOQ= github.com/getsentry/sentry-go v0.35.1/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE= github.com/gkampitakis/ciinfo v0.3.3 h1:28PgAHtW3wG7UCAKuCK+17rBib9iqtLjajuWsVLUPQY= @@ -22,8 +28,13 @@ github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7Lk github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= +github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -31,6 +42,10 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mark3labs/mcp-go v0.41.1 h1:w78eWfiQam2i8ICL7AL0WFiq7KHNJQ6UB53ZVtH4KGA= +github.com/mark3labs/mcp-go v0.41.1/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g= github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= @@ -51,6 +66,8 @@ github.com/segmentio/encoding v0.3.4 h1:WM4IBnxH8B9TakiM2QD5LyNl9JSndh88QbHqVC+P github.com/segmentio/encoding v0.3.4/go.mod h1:n0JeuIqEQrQoPDGsjo8UNd1iA0U8d8+oHAA4E3G3OxM= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -86,6 +103,10 @@ go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= diff --git a/internal/cmd/mcp.go b/internal/cmd/mcp.go new file mode 100644 index 00000000..5b554847 --- /dev/null +++ b/internal/cmd/mcp.go @@ -0,0 +1,22 @@ +package cmd + +import ( + "github.com/formancehq/numscript/internal/mcp_impl" + "github.com/spf13/cobra" +) + +var mcpCmd = &cobra.Command{ + Use: "mcp", + Short: "Run the mcp server", + Hidden: true, + RunE: func(cmd *cobra.Command, args []string) error { + err := mcp_impl.RunServer() + if err != nil { + cmd.SilenceErrors = true + cmd.SilenceUsage = true + return err + } + + return nil + }, +} diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 5eda94f1..1a4d8e19 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -24,6 +24,7 @@ func Execute(options CliOptions) { rootCmd.Version = options.Version rootCmd.AddCommand(lspCmd) + rootCmd.AddCommand(mcpCmd) rootCmd.AddCommand(checkCmd) rootCmd.AddCommand(getTestCmd()) rootCmd.AddCommand(getTestInitCmd()) diff --git a/internal/mcp_impl/handlers.go b/internal/mcp_impl/handlers.go new file mode 100644 index 00000000..bbc6a321 --- /dev/null +++ b/internal/mcp_impl/handlers.go @@ -0,0 +1,118 @@ +package mcp_impl + +import ( + "context" + "fmt" + "math/big" + + "github.com/formancehq/numscript/internal/interpreter" + "github.com/formancehq/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.WithObject("balances", + mcp.Required(), + mcp.Description(`The accounts' balances. A nested map from the account name, to the asset, to its integer amount. + For example: { "alice": { "USD/2": 100, "EUR/2": -42 }, "bob": { "BTC": 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, func(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 { + // TODO return all errors + mcp.NewToolResultError(parsed.Errors[0].Msg) + } + + balancesRaw, ok := request.GetArguments()["balances"].(map[string]any) + if !ok { + return mcp.NewToolResultError(fmt.Sprintf("Expected an object as balances, got: <%#v>", request.GetArguments()["balances"])), nil + } + + iBalances := interpreter.Balances{} + for account, assetsRaw := range balancesRaw { + if iBalances[account] == nil { + iBalances[account] = interpreter.AccountBalance{} + } + + assets, ok := assetsRaw.(map[string]any) + if !ok { + return mcp.NewToolResultError(fmt.Sprintf("Expected nested object for account %v", account)), nil + } + + for asset, amountRaw := range assets { + amount, ok := amountRaw.(float64) + if !ok { + return mcp.NewToolResultError(fmt.Sprintf("Expected float for amount: %v", amountRaw)), nil + } + + n, _ := big.NewFloat(amount).Int(new(big.Int)) + iBalances[account][asset] = n + } + } + + out, iErr := interpreter.RunProgram( + context.Background(), + parsed.Value, + map[string]string{}, + interpreter.StaticStore{ + Balances: interpreter.Balances(iBalances), + }, + map[string]struct{}{}, + ) + if iErr != nil { + mcp.NewToolResultError(iErr.Error()) + } + return mcp.NewToolResultJSON(*out) + }) +} + +func RunServer() error { + // Create a new MCP server + s := server.NewMCPServer( + "Numscript", + "0.0.1", + server.WithToolCapabilities(false), + server.WithRecovery(), + ) + addEvalTool(s) + + // Start the server + if err := server.ServeStdio(s); err != nil { + return err + } + + return nil +} From 1d34f59ab78b0a9f87c5706841e7c8268f7cfe02 Mon Sep 17 00:00:00 2001 From: ascandone Date: Thu, 2 Oct 2025 23:49:07 +0200 Subject: [PATCH 02/10] simplify --- internal/mcp_impl/handlers.go | 60 ++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/internal/mcp_impl/handlers.go b/internal/mcp_impl/handlers.go index bbc6a321..dc818141 100644 --- a/internal/mcp_impl/handlers.go +++ b/internal/mcp_impl/handlers.go @@ -11,6 +11,36 @@ import ( "github.com/mark3labs/mcp-go/server" ) +func parseBalancesJson(balancesRaw any) (interpreter.Balances, *mcp.CallToolResult) { + balances, ok := balancesRaw.(map[string]any) + if !ok { + return interpreter.Balances{}, mcp.NewToolResultError(fmt.Sprintf("Expected an object as balances, got: <%#v>", balancesRaw)) + } + + iBalances := interpreter.Balances{} + for account, assetsRaw := range balances { + if iBalances[account] == nil { + iBalances[account] = interpreter.AccountBalance{} + } + + assets, ok := assetsRaw.(map[string]any) + if !ok { + return interpreter.Balances{}, mcp.NewToolResultError(fmt.Sprintf("Expected nested object for account %v", account)) + } + + for asset, amountRaw := range assets { + amount, ok := amountRaw.(float64) + if !ok { + return interpreter.Balances{}, mcp.NewToolResultError(fmt.Sprintf("Expected float for amount: %v", amountRaw)) + } + + n, _ := big.NewFloat(amount).Int(new(big.Int)) + iBalances[account][asset] = n + } + } + return iBalances, nil +} + func addEvalTool(s *server.MCPServer) { tool := mcp.NewTool("evaluate", mcp.WithDescription("Evaluate a numscript program"), @@ -56,31 +86,9 @@ func addEvalTool(s *server.MCPServer) { mcp.NewToolResultError(parsed.Errors[0].Msg) } - balancesRaw, ok := request.GetArguments()["balances"].(map[string]any) - if !ok { - return mcp.NewToolResultError(fmt.Sprintf("Expected an object as balances, got: <%#v>", request.GetArguments()["balances"])), nil - } - - iBalances := interpreter.Balances{} - for account, assetsRaw := range balancesRaw { - if iBalances[account] == nil { - iBalances[account] = interpreter.AccountBalance{} - } - - assets, ok := assetsRaw.(map[string]any) - if !ok { - return mcp.NewToolResultError(fmt.Sprintf("Expected nested object for account %v", account)), nil - } - - for asset, amountRaw := range assets { - amount, ok := amountRaw.(float64) - if !ok { - return mcp.NewToolResultError(fmt.Sprintf("Expected float for amount: %v", amountRaw)), nil - } - - n, _ := big.NewFloat(amount).Int(new(big.Int)) - iBalances[account][asset] = n - } + balances, mcpErr := parseBalancesJson(request.GetArguments()["balances"]) + if mcpErr != nil { + return mcpErr, nil } out, iErr := interpreter.RunProgram( @@ -88,7 +96,7 @@ func addEvalTool(s *server.MCPServer) { parsed.Value, map[string]string{}, interpreter.StaticStore{ - Balances: interpreter.Balances(iBalances), + Balances: balances, }, map[string]struct{}{}, ) From 14316bc308bca7e89e5a6f7989db33c79d352266 Mon Sep 17 00:00:00 2001 From: ascandone Date: Thu, 2 Oct 2025 23:53:23 +0200 Subject: [PATCH 03/10] show all errs --- internal/mcp_impl/handlers.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/mcp_impl/handlers.go b/internal/mcp_impl/handlers.go index dc818141..9342f237 100644 --- a/internal/mcp_impl/handlers.go +++ b/internal/mcp_impl/handlers.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "math/big" + "strings" "github.com/formancehq/numscript/internal/interpreter" "github.com/formancehq/numscript/internal/parser" @@ -83,7 +84,11 @@ func addEvalTool(s *server.MCPServer) { parsed := parser.Parse(script) if len(parsed.Errors) != 0 { // TODO return all errors - mcp.NewToolResultError(parsed.Errors[0].Msg) + out := make([]string, len(parsed.Errors)) + for index, err := range parsed.Errors { + out[index] = err.Msg + } + mcp.NewToolResultError(strings.Join(out, ", ")) } balances, mcpErr := parseBalancesJson(request.GetArguments()["balances"]) From 5da979182a0e7477400214363c8a0317a7ff060b Mon Sep 17 00:00:00 2001 From: ascandone Date: Fri, 3 Oct 2025 16:05:41 +0200 Subject: [PATCH 04/10] handle vars --- internal/mcp_impl/handlers.go | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/internal/mcp_impl/handlers.go b/internal/mcp_impl/handlers.go index 9342f237..c68e6621 100644 --- a/internal/mcp_impl/handlers.go +++ b/internal/mcp_impl/handlers.go @@ -42,6 +42,26 @@ func parseBalancesJson(balancesRaw any) (interpreter.Balances, *mcp.CallToolResu return iBalances, nil } +func parseVarsJson(varsRaw any) (map[string]string, *mcp.CallToolResult) { + vars, ok := varsRaw.(map[string]any) + if !ok { + return map[string]string{}, mcp.NewToolResultError(fmt.Sprintf("Expected an object as vars, got: <%#v>", varsRaw)) + } + + iVars := map[string]string{} + for key, rawValue := range vars { + + value, ok := rawValue.(string) + if !ok { + return map[string]string{}, mcp.NewToolResultError(fmt.Sprintf("Expected stringified var, got: %v", key)) + } + + iVars[key] = value + } + + return iVars, nil +} + func addEvalTool(s *server.MCPServer) { tool := mcp.NewTool("evaluate", mcp.WithDescription("Evaluate a numscript program"), @@ -75,7 +95,6 @@ func addEvalTool(s *server.MCPServer) { ), ) 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 @@ -96,10 +115,15 @@ func addEvalTool(s *server.MCPServer) { return mcpErr, nil } + vars, mcpErr := parseVarsJson(request.GetArguments()["vars"]) + if mcpErr != nil { + return mcpErr, nil + } + out, iErr := interpreter.RunProgram( context.Background(), parsed.Value, - map[string]string{}, + vars, interpreter.StaticStore{ Balances: balances, }, From f5d731c3784ba0ff1afda92b8d39de1ebabe69f6 Mon Sep 17 00:00:00 2001 From: ascandone Date: Fri, 3 Oct 2025 16:23:48 +0200 Subject: [PATCH 05/10] add check --- internal/analysis/diagnostic_kind.go | 15 +++++++++++ internal/mcp_impl/handlers.go | 37 ++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/internal/analysis/diagnostic_kind.go b/internal/analysis/diagnostic_kind.go index 8b2a376b..e16330a3 100644 --- a/internal/analysis/diagnostic_kind.go +++ b/internal/analysis/diagnostic_kind.go @@ -36,6 +36,21 @@ func SeverityToAnsiString(s Severity) string { } } +func SeverityToString(s Severity) string { + switch s { + case ErrorSeverity: + return "Error" + case WarningSeverity: + return "Warning" + case Information: + return "Info" + case Hint: + return "Hint" + default: + return utils.NonExhaustiveMatchPanic[string](s) + } +} + type DiagnosticKind interface { Message() string Severity() Severity diff --git a/internal/mcp_impl/handlers.go b/internal/mcp_impl/handlers.go index c68e6621..90700cc2 100644 --- a/internal/mcp_impl/handlers.go +++ b/internal/mcp_impl/handlers.go @@ -6,6 +6,7 @@ import ( "math/big" "strings" + "github.com/formancehq/numscript/internal/analysis" "github.com/formancehq/numscript/internal/interpreter" "github.com/formancehq/numscript/internal/parser" "github.com/mark3labs/mcp-go/mcp" @@ -136,6 +137,41 @@ func addEvalTool(s *server.MCPServer) { }) } +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( @@ -145,6 +181,7 @@ func RunServer() error { server.WithRecovery(), ) addEvalTool(s) + addCheckTool(s) // Start the server if err := server.ServeStdio(s); err != nil { From a0718eb59c1bac59710b6dbf1582cd471c5e0ea6 Mon Sep 17 00:00:00 2001 From: ascandone Date: Fri, 3 Oct 2025 16:35:43 +0200 Subject: [PATCH 06/10] add instructions --- internal/mcp_impl/handlers.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/mcp_impl/handlers.go b/internal/mcp_impl/handlers.go index 90700cc2..6384fecd 100644 --- a/internal/mcp_impl/handlers.go +++ b/internal/mcp_impl/handlers.go @@ -179,6 +179,10 @@ func RunServer() error { "0.0.1", server.WithToolCapabilities(false), server.WithRecovery(), + server.WithInstructions(` + You're a numscript expert AI assistant. Numscript is a DSL that allows modelling finantial transaction in an easy and declarative way. Numscript scripts alwasy terminate. + `), + // TODO add prompt ) addEvalTool(s) addCheckTool(s) From 95c0aa2ec93faaef1c17a91a52b1e4ac2d15b836 Mon Sep 17 00:00:00 2001 From: ascandone Date: Tue, 7 Oct 2025 10:15:33 +0200 Subject: [PATCH 07/10] removed comment --- internal/mcp_impl/handlers.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/mcp_impl/handlers.go b/internal/mcp_impl/handlers.go index 6384fecd..d113aeb7 100644 --- a/internal/mcp_impl/handlers.go +++ b/internal/mcp_impl/handlers.go @@ -103,7 +103,6 @@ func addEvalTool(s *server.MCPServer) { parsed := parser.Parse(script) if len(parsed.Errors) != 0 { - // TODO return all errors out := make([]string, len(parsed.Errors)) for index, err := range parsed.Errors { out[index] = err.Msg From c3e2ab0969fea59b85e916711ee774a737a7ed92 Mon Sep 17 00:00:00 2001 From: ascandone Date: Tue, 7 Oct 2025 15:02:29 +0200 Subject: [PATCH 08/10] fix: fix context --- internal/mcp_impl/handlers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/mcp_impl/handlers.go b/internal/mcp_impl/handlers.go index d113aeb7..d0649f76 100644 --- a/internal/mcp_impl/handlers.go +++ b/internal/mcp_impl/handlers.go @@ -121,7 +121,7 @@ func addEvalTool(s *server.MCPServer) { } out, iErr := interpreter.RunProgram( - context.Background(), + ctx, parsed.Value, vars, interpreter.StaticStore{ From a07c41f4b1ee5a39f5d9b4e41879a366620ccc2f Mon Sep 17 00:00:00 2001 From: ascandone Date: Tue, 28 Oct 2025 08:27:33 +0100 Subject: [PATCH 09/10] fix typo --- internal/mcp_impl/handlers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/mcp_impl/handlers.go b/internal/mcp_impl/handlers.go index d0649f76..14d478bb 100644 --- a/internal/mcp_impl/handlers.go +++ b/internal/mcp_impl/handlers.go @@ -179,7 +179,7 @@ func RunServer() error { server.WithToolCapabilities(false), server.WithRecovery(), server.WithInstructions(` - You're a numscript expert AI assistant. Numscript is a DSL that allows modelling finantial transaction in an easy and declarative way. Numscript scripts alwasy terminate. + 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. `), // TODO add prompt ) From 240e7136b55b20af69fde4bee3ab6f343324850d Mon Sep 17 00:00:00 2001 From: ascandone Date: Tue, 28 Oct 2025 08:32:12 +0100 Subject: [PATCH 10/10] better err --- internal/mcp_impl/handlers.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/mcp_impl/handlers.go b/internal/mcp_impl/handlers.go index 14d478bb..93139eb9 100644 --- a/internal/mcp_impl/handlers.go +++ b/internal/mcp_impl/handlers.go @@ -54,7 +54,7 @@ func parseVarsJson(varsRaw any) (map[string]string, *mcp.CallToolResult) { value, ok := rawValue.(string) if !ok { - return map[string]string{}, mcp.NewToolResultError(fmt.Sprintf("Expected stringified var, got: %v", key)) + return map[string]string{}, mcp.NewToolResultError(fmt.Sprintf("Expected %s var to be a string, got: %T instead", key, rawValue)) } iVars[key] = value @@ -130,7 +130,7 @@ func addEvalTool(s *server.MCPServer) { map[string]struct{}{}, ) if iErr != nil { - mcp.NewToolResultError(iErr.Error()) + return mcp.NewToolResultError(iErr.Error()), nil } return mcp.NewToolResultJSON(*out) }) @@ -181,7 +181,6 @@ func RunServer() error { 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. `), - // TODO add prompt ) addEvalTool(s) addCheckTool(s)