Skip to content

Commit 490f7ac

Browse files
feat: add sandbox MCP tools
1 parent 3a7994f commit 490f7ac

14 files changed

Lines changed: 475 additions & 3 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ port: 8080
151151
base_url: https://your-domain.com
152152
authorization_server_url: https://your-auth-server.com
153153
api_base_url: https://api.createos.nodeops.network
154+
sandbox_api_base_url: https://your-fc-control-api.example.com
154155
transport: http # "http" or "stdio"
155156
log_level: debug
156157

config-files/config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ port: 8080
22
base_url:
33
authorization_server_url:
44
api_base_url:
5+
sandbox_api_base_url:
56
transport: http
67
log_level: debug
78

config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type Config struct {
1212
BaseURL string `yaml:"base_url"`
1313
AuthorizationServerUrl string `yaml:"authorization_server_url"`
1414
APIBaseUrl string `yaml:"api_base_url"`
15+
SandboxAPIBaseUrl string `yaml:"sandbox_api_base_url"`
1516
Transport string `yaml:"transport"`
1617
LogLevel string `yaml:"log_level"`
1718

handlers/CreateSandbox.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package handler
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
mcputils "github.com/NodeOps-app/createos-mcp/helpers"
8+
"github.com/mark3labs/mcp-go/mcp"
9+
)
10+
11+
type CreateSandboxParams struct {
12+
Body map[string]interface{} `json:"body"`
13+
}
14+
15+
func CreateSandboxHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
16+
authInfo, args, err := handleRequest(ctx, request)
17+
if err != nil {
18+
return nil, err
19+
}
20+
21+
params, err := mcputils.ParamsParser[CreateSandboxParams](args)
22+
if err != nil {
23+
return nil, fmt.Errorf("failed to parse parameters: %w", err)
24+
}
25+
26+
return makeSandboxPostRequest("/v1/sandboxes", params.Body, authInfo)
27+
}

handlers/DeleteSandbox.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package handler
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
mcputils "github.com/NodeOps-app/createos-mcp/helpers"
8+
"github.com/mark3labs/mcp-go/mcp"
9+
)
10+
11+
type DeleteSandboxParams struct {
12+
ID string `json:"id"`
13+
}
14+
15+
func DeleteSandboxHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
16+
authInfo, args, err := handleRequest(ctx, request)
17+
if err != nil {
18+
return nil, err
19+
}
20+
21+
params, err := mcputils.ParamsParser[DeleteSandboxParams](args)
22+
if err != nil {
23+
return nil, fmt.Errorf("failed to parse parameters: %w", err)
24+
}
25+
26+
return makeSandboxDeleteRequest(fmt.Sprintf("/v1/sandboxes/%s", params.ID), authInfo)
27+
}

handlers/ExecSandbox.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package handler
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
mcputils "github.com/NodeOps-app/createos-mcp/helpers"
8+
"github.com/mark3labs/mcp-go/mcp"
9+
)
10+
11+
type ExecSandboxParams struct {
12+
ID string `json:"id"`
13+
Body map[string]interface{} `json:"body"`
14+
}
15+
16+
func ExecSandboxHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
17+
authInfo, args, err := handleRequest(ctx, request)
18+
if err != nil {
19+
return nil, err
20+
}
21+
22+
params, err := mcputils.ParamsParser[ExecSandboxParams](args)
23+
if err != nil {
24+
return nil, fmt.Errorf("failed to parse parameters: %w", err)
25+
}
26+
if stream, ok := params.Body["stream"].(bool); ok && stream {
27+
return nil, fmt.Errorf("streaming exec is not supported by ExecSandbox yet; omit stream or set it to false")
28+
}
29+
30+
return makeSandboxPostRequest(fmt.Sprintf("/v1/sandboxes/%s/exec", params.ID), params.Body, authInfo)
31+
}

handlers/UpdateSandbox.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package handler
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
mcputils "github.com/NodeOps-app/createos-mcp/helpers"
8+
"github.com/mark3labs/mcp-go/mcp"
9+
)
10+
11+
type UpdateSandboxParams struct {
12+
ID string `json:"id"`
13+
Body map[string]interface{} `json:"body"`
14+
}
15+
16+
func UpdateSandboxHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
17+
authInfo, args, err := handleRequest(ctx, request)
18+
if err != nil {
19+
return nil, err
20+
}
21+
22+
params, err := mcputils.ParamsParser[UpdateSandboxParams](args)
23+
if err != nil {
24+
return nil, fmt.Errorf("failed to parse parameters: %w", err)
25+
}
26+
27+
return makeSandboxPatchRequest(fmt.Sprintf("/v1/sandboxes/%s", params.ID), params.Body, authInfo)
28+
}

handlers/helpers.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,42 @@ func makeDeleteRequest(path string, authInfo *AuthInfo) (*mcp.CallToolResult, er
8686
},
8787
}, nil
8888
}
89+
90+
func makeSandboxPostRequest(path string, body interface{}, authInfo *AuthInfo) (*mcp.CallToolResult, error) {
91+
resp, err := mcputils.SandboxPost(path, body, authInfo.Method, authInfo.Value)
92+
if err != nil {
93+
return nil, err
94+
}
95+
96+
return &mcp.CallToolResult{
97+
Content: []mcp.Content{
98+
mcp.NewTextContent(string(resp.Body())),
99+
},
100+
}, nil
101+
}
102+
103+
func makeSandboxPatchRequest(path string, body interface{}, authInfo *AuthInfo) (*mcp.CallToolResult, error) {
104+
resp, err := mcputils.SandboxPatch(path, body, authInfo.Method, authInfo.Value)
105+
if err != nil {
106+
return nil, err
107+
}
108+
109+
return &mcp.CallToolResult{
110+
Content: []mcp.Content{
111+
mcp.NewTextContent(string(resp.Body())),
112+
},
113+
}, nil
114+
}
115+
116+
func makeSandboxDeleteRequest(path string, authInfo *AuthInfo) (*mcp.CallToolResult, error) {
117+
resp, err := mcputils.SandboxDelete(path, authInfo.Method, authInfo.Value)
118+
if err != nil {
119+
return nil, err
120+
}
121+
122+
return &mcp.CallToolResult{
123+
Content: []mcp.Content{
124+
mcp.NewTextContent(string(resp.Body())),
125+
},
126+
}, nil
127+
}

helpers/httpclient.go

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,39 @@ import (
1111

1212
// Client returns a configured Resty client with base URL and auth token
1313
func Client() *resty.Client {
14-
baseURL := config.Cfg.APIBaseUrl
15-
1614
client := resty.New().
17-
SetBaseURL(baseURL).
15+
SetBaseURL(config.Cfg.APIBaseUrl).
1816
SetHeader("Content-Type", "application/json").
1917
SetHeader("Accept", "application/json")
2018

2119
return client
2220
}
2321

22+
func SandboxClient() (*resty.Client, error) {
23+
if config.Cfg.SandboxAPIBaseUrl == "" {
24+
return nil, fmt.Errorf("sandbox_api_base_url is required for sandbox tools")
25+
}
26+
27+
client := resty.New().
28+
SetBaseURL(config.Cfg.SandboxAPIBaseUrl).
29+
SetHeader("Content-Type", "application/json").
30+
SetHeader("Accept", "application/json")
31+
32+
return client, nil
33+
}
34+
35+
func setAuth(req *resty.Request, authMethod string, authValue string) error {
36+
switch authMethod {
37+
case "api-key":
38+
req.SetHeader("X-Api-Key", authValue)
39+
case "bearer-token":
40+
req.SetHeader("X-Access-Token", authValue)
41+
default:
42+
return fmt.Errorf("unsupported auth method: %s", authMethod)
43+
}
44+
return nil
45+
}
46+
2447
// Get makes a GET request with authentication
2548
func Get(path string, queryParams map[string]string, authMethod string, authValue string) (*resty.Response, error) {
2649
req := Client().R()
@@ -180,3 +203,69 @@ func DeleteWithBody(path string, body interface{}, authMethod string, authValue
180203

181204
return resp, nil
182205
}
206+
207+
// SandboxPost makes a POST request to the sandbox API with authentication.
208+
func SandboxPost(path string, body interface{}, authMethod string, authValue string) (*resty.Response, error) {
209+
client, err := SandboxClient()
210+
if err != nil {
211+
return nil, err
212+
}
213+
req := client.R().SetBody(body)
214+
if err := setAuth(req, authMethod, authValue); err != nil {
215+
return nil, err
216+
}
217+
218+
resp, err := req.Post(path)
219+
if err != nil {
220+
return nil, fmt.Errorf("sandbox POST request failed: %w", err)
221+
}
222+
if resp.IsError() {
223+
return resp, fmt.Errorf("sandbox API error (status %d)", resp.StatusCode())
224+
}
225+
226+
return resp, nil
227+
}
228+
229+
// SandboxPatch makes a PATCH request to the sandbox API with authentication.
230+
func SandboxPatch(path string, body interface{}, authMethod string, authValue string) (*resty.Response, error) {
231+
client, err := SandboxClient()
232+
if err != nil {
233+
return nil, err
234+
}
235+
req := client.R().SetBody(body)
236+
if err := setAuth(req, authMethod, authValue); err != nil {
237+
return nil, err
238+
}
239+
240+
resp, err := req.Patch(path)
241+
if err != nil {
242+
return nil, fmt.Errorf("sandbox PATCH request failed: %w", err)
243+
}
244+
if resp.IsError() {
245+
return resp, fmt.Errorf("sandbox API error (status %d)", resp.StatusCode())
246+
}
247+
248+
return resp, nil
249+
}
250+
251+
// SandboxDelete makes a DELETE request to the sandbox API with authentication.
252+
func SandboxDelete(path string, authMethod string, authValue string) (*resty.Response, error) {
253+
client, err := SandboxClient()
254+
if err != nil {
255+
return nil, err
256+
}
257+
req := client.R()
258+
if err := setAuth(req, authMethod, authValue); err != nil {
259+
return nil, err
260+
}
261+
262+
resp, err := req.Delete(path)
263+
if err != nil {
264+
return nil, fmt.Errorf("sandbox DELETE request failed: %w", err)
265+
}
266+
if resp.IsError() {
267+
return resp, fmt.Errorf("sandbox API error (status %d)", resp.StatusCode())
268+
}
269+
270+
return resp, nil
271+
}

0 commit comments

Comments
 (0)