Skip to content

Commit f316d03

Browse files
Add mcp UI resource and codemode (#179)
* add mcp-ui example * add codemode example * add mcp-discover prompt
1 parent d236011 commit f316d03

File tree

175 files changed

+213265
-2
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

175 files changed

+213265
-2
lines changed

go.mod

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
github.com/docker/cli-docs-tool v0.10.0
1212
github.com/docker/docker v28.2.2+incompatible
1313
github.com/docker/docker-credential-helpers v0.9.3
14+
github.com/dop251/goja v0.0.0-20251008123653-cf18d89f3cf6
1415
github.com/fsnotify/fsnotify v1.9.0
1516
github.com/google/go-containerregistry v0.20.6
1617
github.com/google/jsonschema-go v0.3.0
@@ -35,6 +36,12 @@ require (
3536
gopkg.in/yaml.v3 v3.0.1
3637
)
3738

39+
require (
40+
github.com/dlclark/regexp2 v1.11.4 // indirect
41+
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
42+
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
43+
)
44+
3845
require (
3946
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
4047
github.com/PaesslerAG/gval v1.2.4 // indirect
@@ -116,7 +123,7 @@ require (
116123
github.com/onsi/gomega v1.37.0 // indirect
117124
github.com/open-policy-agent/opa v1.5.1 // indirect
118125
github.com/opentracing/opentracing-go v1.2.0 // indirect
119-
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
126+
github.com/pelletier/go-toml/v2 v2.2.4
120127
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
121128
github.com/prometheus/client_model v0.6.2 // indirect
122129
github.com/rivo/uniseg v0.4.7 // indirect

go.sum

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBp
5555
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
5656
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
5757
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
58+
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
59+
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
5860
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
5961
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
6062
github.com/PaesslerAG/gval v1.0.0/go.mod h1:y/nm5yEyTeX6av0OfKJNp9rBNj2XrGhAf5+v24IBN1I=
@@ -218,6 +220,8 @@ github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi
218220
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
219221
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
220222
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
223+
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
224+
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
221225
github.com/docker/cli v28.2.2+incompatible h1:qzx5BNUDFqlvyq4AHzdNB7gSyVTmU4cgsyN9SdInc1A=
222226
github.com/docker/cli v28.2.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
223227
github.com/docker/cli-docs-tool v0.10.0 h1:bOD6mKynPQgojQi3s2jgcUWGp/Ebqy1SeCr9VfKQLLU=
@@ -240,6 +244,8 @@ github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHz
240244
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
241245
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
242246
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
247+
github.com/dop251/goja v0.0.0-20251008123653-cf18d89f3cf6 h1:6dE1TmjqkY6tehR4A67gDNhvDtuZ54ocu7ab4K9o540=
248+
github.com/dop251/goja v0.0.0-20251008123653-cf18d89f3cf6/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
243249
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
244250
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
245251
github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM=
@@ -303,6 +309,8 @@ github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3Bum
303309
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
304310
github.com/go-piv/piv-go/v2 v2.3.0 h1:kKkrYlgLQTMPA6BiSL25A7/x4CEh2YCG7rtb/aTkx+g=
305311
github.com/go-piv/piv-go/v2 v2.3.0/go.mod h1:ShZi74nnrWNQEdWzRUd/3cSig3uNOcEZp+EWl0oewnI=
312+
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
313+
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
306314
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
307315
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
308316
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
@@ -369,6 +377,8 @@ github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
369377
github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
370378
github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
371379
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
380+
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
381+
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
372382
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
373383
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
374384
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
@@ -903,6 +913,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
903913
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
904914
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
905915
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
916+
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
906917
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
907918
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
908919
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

pkg/codemode/codemode.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package codemode
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"strings"
8+
9+
"github.com/modelcontextprotocol/go-sdk/mcp"
10+
)
11+
12+
const Prompt = `Run a Javascript script to call MCP tools.
13+
14+
Instead of calling individual tools directly, use this to write a Javascript script that calls as many tools as needed.
15+
This allows you to combine multiple tool calls in a single request, perform conditional logic,
16+
and manipulate the results before returning them.
17+
18+
Instructions:
19+
- The script has access to all the tools as plain javascript functions.
20+
- "await"/"async" are never needed. All the tool calls are synchronous.
21+
- Every tool function returns a string result.
22+
- The script must return a string result.
23+
24+
Available tools/functions:
25+
26+
`
27+
28+
type RunToolsWithJavascriptArgs struct {
29+
Script string `json:"script"`
30+
}
31+
32+
// ToolSet represents a collection of MCP tools with lifecycle management
33+
type ToolSet interface {
34+
Tools(ctx context.Context) ([]*ToolWithHandler, error)
35+
}
36+
37+
// ToolWithHandler combines an MCP Tool definition with its handler function
38+
type ToolWithHandler struct {
39+
Tool *mcp.Tool
40+
Handler mcp.ToolHandler
41+
}
42+
43+
type tool struct {
44+
toolsets []ToolSet
45+
}
46+
47+
func (c *tool) Tools(ctx context.Context) ([]*ToolWithHandler, error) {
48+
var functionsDoc []string
49+
50+
for _, toolset := range c.toolsets {
51+
allTools, err := toolset.Tools(ctx)
52+
if err != nil {
53+
return nil, err
54+
}
55+
56+
for _, tool := range allTools {
57+
functionsDoc = append(functionsDoc, toolToJsDoc(tool.Tool))
58+
}
59+
}
60+
61+
description := Prompt + strings.Join(functionsDoc, "\n")
62+
63+
jstool := &ToolWithHandler{
64+
Tool: &mcp.Tool{
65+
Name: "run_tools_with_javascript",
66+
Description: description,
67+
Annotations: &mcp.ToolAnnotations{
68+
Title: "Run tools with Javascript",
69+
},
70+
InputSchema: map[string]any{
71+
"type": "object",
72+
"required": []string{"script"},
73+
"properties": map[string]any{
74+
"script": map[string]any{
75+
"type": "string",
76+
"description": "script to execute",
77+
},
78+
},
79+
},
80+
},
81+
Handler: func(ctx context.Context, request *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
82+
var args RunToolsWithJavascriptArgs
83+
if err := json.Unmarshal(request.Params.Arguments, &args); err != nil {
84+
return nil, fmt.Errorf("parsing arguments: %w", err)
85+
}
86+
87+
output, err := c.runJavascript(ctx, args.Script)
88+
if err != nil {
89+
return nil, err
90+
}
91+
92+
return &mcp.CallToolResult{
93+
Content: []mcp.Content{
94+
&mcp.TextContent{Text: output},
95+
},
96+
}, nil
97+
},
98+
}
99+
100+
return []*ToolWithHandler{jstool}, nil
101+
}
102+
103+
func Wrap(toolsets []ToolSet) ToolSet {
104+
return &tool{
105+
toolsets: toolsets,
106+
}
107+
}

pkg/codemode/console.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package codemode
2+
3+
import (
4+
"fmt"
5+
"os"
6+
)
7+
8+
func console() map[string]any {
9+
return map[string]any{
10+
"debug": consoleDebug,
11+
"error": consoleError,
12+
"info": consoleInfo,
13+
"log": consoleLog,
14+
"trace": consoleTrace,
15+
"warn": consoleWarn,
16+
}
17+
}
18+
19+
func consoleDebug(args ...any) {
20+
fmt.Fprintln(os.Stdout, args...)
21+
}
22+
23+
func consoleError(args ...any) {
24+
fmt.Fprintln(os.Stdout, args...)
25+
}
26+
27+
func consoleInfo(args ...any) {
28+
fmt.Fprintln(os.Stdout, args...)
29+
}
30+
31+
func consoleLog(args ...any) {
32+
fmt.Fprintln(os.Stdout, args...)
33+
}
34+
35+
func consoleTrace(args ...any) {
36+
fmt.Fprintln(os.Stdout, args...)
37+
}
38+
39+
func consoleWarn(args ...any) {
40+
fmt.Fprintln(os.Stdout, args...)
41+
}

pkg/codemode/exec.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package codemode
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"slices"
8+
9+
"github.com/dop251/goja"
10+
"github.com/modelcontextprotocol/go-sdk/mcp"
11+
)
12+
13+
func (c *tool) runJavascript(ctx context.Context, script string) (string, error) {
14+
vm := goja.New()
15+
16+
// Inject console object to the help the LLM debug its own code.
17+
_ = vm.Set("console", console())
18+
19+
// Inject every tool as a javascript function.
20+
for _, toolset := range c.toolsets {
21+
allTools, err := toolset.Tools(ctx)
22+
if err != nil {
23+
return "", err
24+
}
25+
26+
for _, toolWithHandler := range allTools {
27+
_ = vm.Set(toolWithHandler.Tool.Name, callTool(ctx, toolWithHandler))
28+
}
29+
}
30+
31+
// Wrap the user script in an IIFE to allow top-level returns.
32+
script = "(() => {\n" + script + "\n})()"
33+
34+
// Run the script.
35+
v, err := vm.RunString(script)
36+
if err != nil {
37+
return fmt.Sprintf("Error running script: %s", err), nil
38+
}
39+
40+
// Some script are fire and forget and don't return anything.
41+
// In that case we return "done." to please the LLM which can't deal with empty responses.
42+
result := v.Export()
43+
if result == nil {
44+
return "<no output>", nil
45+
}
46+
47+
return fmt.Sprintf("%v", result), nil
48+
}
49+
50+
func callTool(ctx context.Context, toolWithHandler *ToolWithHandler) func(args map[string]any) (string, error) {
51+
return func(args map[string]any) (string, error) {
52+
// Extract required fields from InputSchema
53+
var required []string
54+
if toolWithHandler.Tool.InputSchema != nil {
55+
if schemaMap, ok := toolWithHandler.Tool.InputSchema.(map[string]any); ok {
56+
if req, ok := schemaMap["required"].([]any); ok {
57+
for _, r := range req {
58+
if reqStr, ok := r.(string); ok {
59+
required = append(required, reqStr)
60+
}
61+
}
62+
} else if req, ok := schemaMap["required"].([]string); ok {
63+
required = req
64+
}
65+
}
66+
}
67+
68+
nonNilArgs := make(map[string]any)
69+
for k, v := range args {
70+
if slices.Contains(required, k) || v != nil {
71+
nonNilArgs[k] = v
72+
}
73+
}
74+
75+
arguments, err := json.Marshal(nonNilArgs)
76+
if err != nil {
77+
return "", err
78+
}
79+
80+
result, err := toolWithHandler.Handler(ctx, &mcp.CallToolRequest{
81+
Params: &mcp.CallToolParamsRaw{
82+
Name: toolWithHandler.Tool.Name,
83+
Arguments: arguments,
84+
},
85+
})
86+
if err != nil {
87+
return "", err
88+
}
89+
90+
// Extract text from Content
91+
if len(result.Content) > 0 {
92+
if textContent, ok := result.Content[0].(*mcp.TextContent); ok {
93+
return textContent.Text, nil
94+
}
95+
}
96+
97+
return "", nil
98+
}
99+
}

0 commit comments

Comments
 (0)