Skip to content

Commit 9753efd

Browse files
Add create-profile tool
1 parent ee89771 commit 9753efd

File tree

4 files changed

+287
-56
lines changed

4 files changed

+287
-56
lines changed

pkg/gateway/createprofile.go

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package gateway
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"encoding/json"
7+
"errors"
8+
"fmt"
9+
10+
"github.com/modelcontextprotocol/go-sdk/mcp"
11+
12+
"github.com/docker/mcp-gateway/pkg/db"
13+
"github.com/docker/mcp-gateway/pkg/log"
14+
"github.com/docker/mcp-gateway/pkg/oci"
15+
"github.com/docker/mcp-gateway/pkg/workingset"
16+
)
17+
18+
func createProfileHandler(g *Gateway) mcp.ToolHandler {
19+
return func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
20+
// Parse parameters
21+
var params struct {
22+
Name string `json:"name"`
23+
}
24+
25+
if req.Params.Arguments == nil {
26+
return nil, fmt.Errorf("missing arguments")
27+
}
28+
29+
paramsBytes, err := json.Marshal(req.Params.Arguments)
30+
if err != nil {
31+
return nil, fmt.Errorf("failed to marshal arguments: %w", err)
32+
}
33+
34+
if err := json.Unmarshal(paramsBytes, &params); err != nil {
35+
return nil, fmt.Errorf("failed to parse arguments: %w", err)
36+
}
37+
38+
if params.Name == "" {
39+
return nil, fmt.Errorf("name parameter is required")
40+
}
41+
42+
profileName := params.Name
43+
44+
// Create DAO and OCI service
45+
dao, err := db.New()
46+
if err != nil {
47+
return nil, fmt.Errorf("failed to create database client: %w", err)
48+
}
49+
50+
ociService := oci.NewService()
51+
52+
// Build the working set from current gateway state
53+
servers := make([]workingset.Server, 0, len(g.configuration.serverNames))
54+
for _, serverName := range g.configuration.serverNames {
55+
catalogServer, found := g.configuration.servers[serverName]
56+
if !found {
57+
log.Logf("Warning: server %s not found in catalog, skipping", serverName)
58+
continue
59+
}
60+
61+
// Determine server type based on whether it has an image
62+
serverType := workingset.ServerTypeImage
63+
if catalogServer.Image == "" {
64+
// Skip servers without images for now (registry servers)
65+
log.Logf("Warning: server %s has no image, skipping", serverName)
66+
continue
67+
}
68+
69+
// Get config for this server
70+
serverConfig := g.configuration.config[serverName]
71+
if serverConfig == nil {
72+
serverConfig = make(map[string]any)
73+
}
74+
75+
// Get tools for this server
76+
var serverTools []string
77+
if g.configuration.tools.ServerTools != nil {
78+
serverTools = g.configuration.tools.ServerTools[serverName]
79+
}
80+
81+
// Create server entry
82+
server := workingset.Server{
83+
Type: serverType,
84+
Image: catalogServer.Image,
85+
Config: serverConfig,
86+
Secrets: "default",
87+
Tools: serverTools,
88+
Snapshot: &workingset.ServerSnapshot{
89+
Server: catalogServer,
90+
},
91+
}
92+
93+
servers = append(servers, server)
94+
}
95+
96+
if len(servers) == 0 {
97+
return &mcp.CallToolResult{
98+
Content: []mcp.Content{&mcp.TextContent{
99+
Text: "No servers with images found in current gateway state. Cannot create profile.",
100+
}},
101+
IsError: true,
102+
}, nil
103+
}
104+
105+
// Add default secrets
106+
secrets := make(map[string]workingset.Secret)
107+
secrets["default"] = workingset.Secret{
108+
Provider: workingset.SecretProviderDockerDesktop,
109+
}
110+
111+
// Check if profile already exists
112+
existingProfile, err := dao.GetWorkingSet(ctx, profileName)
113+
isUpdate := false
114+
profileID := profileName
115+
116+
if err != nil {
117+
if !errors.Is(err, sql.ErrNoRows) {
118+
return nil, fmt.Errorf("failed to check for existing profile: %w", err)
119+
}
120+
// Profile doesn't exist, we'll create it
121+
} else {
122+
// Profile exists, we'll update it
123+
isUpdate = true
124+
profileID = existingProfile.ID
125+
}
126+
127+
// Create working set
128+
ws := workingset.WorkingSet{
129+
Version: workingset.CurrentWorkingSetVersion,
130+
ID: profileID,
131+
Name: profileName,
132+
Servers: servers,
133+
Secrets: secrets,
134+
}
135+
136+
// Ensure snapshots are resolved
137+
if err := ws.EnsureSnapshotsResolved(ctx, ociService); err != nil {
138+
return nil, fmt.Errorf("failed to resolve snapshots: %w", err)
139+
}
140+
141+
// Validate the working set
142+
if err := ws.Validate(); err != nil {
143+
return &mcp.CallToolResult{
144+
Content: []mcp.Content{&mcp.TextContent{
145+
Text: fmt.Sprintf("Profile validation failed: %v", err),
146+
}},
147+
IsError: true,
148+
}, nil
149+
}
150+
151+
// Create or update the profile
152+
if isUpdate {
153+
if err := dao.UpdateWorkingSet(ctx, ws.ToDb()); err != nil {
154+
return nil, fmt.Errorf("failed to update profile: %w", err)
155+
}
156+
log.Logf("Updated profile %s with %d servers", profileID, len(servers))
157+
} else {
158+
if err := dao.CreateWorkingSet(ctx, ws.ToDb()); err != nil {
159+
return nil, fmt.Errorf("failed to create profile: %w", err)
160+
}
161+
log.Logf("Created profile %s with %d servers", profileID, len(servers))
162+
}
163+
164+
// Build success message
165+
var resultMessage string
166+
if isUpdate {
167+
resultMessage = fmt.Sprintf("Successfully updated profile '%s' (ID: %s) with %d servers:\n", profileName, profileID, len(servers))
168+
} else {
169+
resultMessage = fmt.Sprintf("Successfully created profile '%s' (ID: %s) with %d servers:\n", profileName, profileID, len(servers))
170+
}
171+
172+
// List the servers in the profile
173+
for i, server := range servers {
174+
serverName := server.Snapshot.Server.Name
175+
resultMessage += fmt.Sprintf("\n%d. %s", i+1, serverName)
176+
if server.Image != "" {
177+
resultMessage += fmt.Sprintf(" (image: %s)", server.Image)
178+
}
179+
if len(server.Tools) > 0 {
180+
resultMessage += fmt.Sprintf(" - %d tools", len(server.Tools))
181+
}
182+
if len(server.Config) > 0 {
183+
resultMessage += " - configured"
184+
}
185+
}
186+
187+
return &mcp.CallToolResult{
188+
Content: []mcp.Content{&mcp.TextContent{
189+
Text: resultMessage,
190+
}},
191+
}, nil
192+
}
193+
}

pkg/gateway/dynamic_mcps.go

Lines changed: 84 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,11 @@ import (
2727
// mcpFindTool implements a tool for finding MCP servers in the catalog
2828
func (g *Gateway) createMcpFindTool(_ Configuration, handler mcp.ToolHandler) *ToolRegistration {
2929
tool := &mcp.Tool{
30-
Name: "mcp-find",
31-
Description: "Find MCP servers in the current catalog by name, title, or description. If the user is looking for new capabilities, use this tool to search the MCP catalog for servers that should potentially be enabled. This will not enable the server but will return information about servers that could be enabled. If we find an mcp server, it can be added with the mcp-add tool, and configured with mcp-config-set.",
30+
Name: "mcp-find",
31+
Description: `Find MCP servers in the current catalog by name, title, or description.
32+
If the user is looking for new capabilities, use this tool to search the MCP catalog for servers that should potentially be enabled.
33+
This will not enable the server but will return information about servers that could be enabled.
34+
If we find an mcp server, it can be added with the mcp-add tool, and configured with mcp-config-set.`,
3235
InputSchema: &jsonschema.Schema{
3336
Type: "object",
3437
Properties: map[string]*jsonschema.Schema{
@@ -51,6 +54,85 @@ func (g *Gateway) createMcpFindTool(_ Configuration, handler mcp.ToolHandler) *T
5154
}
5255
}
5356

57+
func (g *Gateway) createMcpAddTool(clientConfig *clientConfig) *ToolRegistration {
58+
tool := &mcp.Tool{
59+
Name: "mcp-add",
60+
Description: `Add a new MCP server to the session.
61+
The server must exist in the catalog.`,
62+
InputSchema: &jsonschema.Schema{
63+
Type: "object",
64+
Properties: map[string]*jsonschema.Schema{
65+
"name": {
66+
Type: "string",
67+
Description: "Name of the MCP server to add to the registry (must exist in catalog)",
68+
},
69+
"activate": {
70+
Type: "boolean",
71+
Description: "Activate all of the server's tools in the current session",
72+
},
73+
},
74+
Required: []string{"name"},
75+
},
76+
}
77+
78+
return &ToolRegistration{
79+
Tool: tool,
80+
Handler: withToolTelemetry("mcp-add", addServerHandler(g, clientConfig)),
81+
}
82+
}
83+
84+
// mcpConfigSetTool implements a tool for setting configuration values for MCP servers
85+
func (g *Gateway) createMcpConfigSetTool(_ *clientConfig) *ToolRegistration {
86+
tool := &mcp.Tool{
87+
Name: "mcp-config-set",
88+
Description: `Set configuration for an MCP server.
89+
The config object will be validated against the server's config schema. If validation fails, the error message will include the correct schema.`,
90+
InputSchema: &jsonschema.Schema{
91+
Type: "object",
92+
Properties: map[string]*jsonschema.Schema{
93+
"server": {
94+
Type: "string",
95+
Description: "Name of the MCP server to configure",
96+
},
97+
"config": {
98+
Type: "object",
99+
Description: "Configuration object for the server. This will be validated against the server's config schema.",
100+
},
101+
},
102+
Required: []string{"server", "config"},
103+
},
104+
}
105+
106+
return &ToolRegistration{
107+
Tool: tool,
108+
Handler: withToolTelemetry("mcp-config-set", configSetHandler(g)),
109+
}
110+
}
111+
112+
func (g *Gateway) createMcpCreateProfileTool(_ *clientConfig) *ToolRegistration {
113+
tool := &mcp.Tool{
114+
Name: "mcp-create-profile",
115+
Description: `Create or update a profile with the current gateway state.
116+
A profile is a snapshot of all currently enabled servers and their configurations.
117+
If a profile with the given name already exists, it will be updated with the current state.`,
118+
InputSchema: &jsonschema.Schema{
119+
Type: "object",
120+
Properties: map[string]*jsonschema.Schema{
121+
"name": {
122+
Type: "string",
123+
Description: "Name of the profile to create or update",
124+
},
125+
},
126+
Required: []string{"name"},
127+
},
128+
}
129+
130+
return &ToolRegistration{
131+
Tool: tool,
132+
Handler: withToolTelemetry("mcp-create-profile", createProfileHandler(g)),
133+
}
134+
}
135+
54136
func (g *Gateway) createCodeModeTool(_ *clientConfig) *ToolRegistration {
55137
tool := &mcp.Tool{
56138
Name: "code-mode",
@@ -520,33 +602,6 @@ func (g *Gateway) readServersFromURL(ctx context.Context, url string) (map[strin
520602
return nil, fmt.Errorf("unable to parse response as OCI catalog or direct catalog format")
521603
}
522604

523-
// mcpConfigSetTool implements a tool for setting configuration values for MCP servers
524-
func (g *Gateway) createMcpConfigSetTool(_ *clientConfig) *ToolRegistration {
525-
tool := &mcp.Tool{
526-
Name: "mcp-config-set",
527-
Description: "Set configuration for an MCP server. The config object will be validated against the server's config schema. If validation fails, the error message will include the correct schema.",
528-
InputSchema: &jsonschema.Schema{
529-
Type: "object",
530-
Properties: map[string]*jsonschema.Schema{
531-
"server": {
532-
Type: "string",
533-
Description: "Name of the MCP server to configure",
534-
},
535-
"config": {
536-
Type: "object",
537-
Description: "Configuration object for the server. This will be validated against the server's config schema.",
538-
},
539-
},
540-
Required: []string{"server", "config"},
541-
},
542-
}
543-
544-
return &ToolRegistration{
545-
Tool: tool,
546-
Handler: withToolTelemetry("mcp-config-set", configSetHandler(g)),
547-
}
548-
}
549-
550605
// createMcpExecTool implements a tool for executing tools that exist in the current session
551606
// but may not be returned from listTools calls
552607
func (g *Gateway) createMcpExecTool() *ToolRegistration {

pkg/gateway/mcpadd.go

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -21,28 +21,8 @@ import (
2121
"github.com/docker/mcp-gateway/pkg/oci"
2222
)
2323

24-
// mcpAddTool implements a tool for adding new servers to the registry
25-
func (g *Gateway) createMcpAddTool(clientConfig *clientConfig) *ToolRegistration {
26-
tool := &mcp.Tool{
27-
Name: "mcp-add",
28-
Description: "Add a new MCP server to the session. The server must exist in the catalog.",
29-
InputSchema: &jsonschema.Schema{
30-
Type: "object",
31-
Properties: map[string]*jsonschema.Schema{
32-
"name": {
33-
Type: "string",
34-
Description: "Name of the MCP server to add to the registry (must exist in catalog)",
35-
},
36-
"activate": {
37-
Type: "boolean",
38-
Description: "Activate all of the server's tools in the current session",
39-
},
40-
},
41-
Required: []string{"name"},
42-
},
43-
}
44-
45-
handler := func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
24+
func addServerHandler(g *Gateway, clientConfig *clientConfig) mcp.ToolHandler {
25+
return func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
4626
// Parse parameters
4727
var params struct {
4828
Name string `json:"name"`
@@ -307,13 +287,10 @@ func (g *Gateway) createMcpAddTool(clientConfig *clientConfig) *ToolRegistration
307287
}},
308288
}, nil
309289
}
310-
311-
return &ToolRegistration{
312-
Tool: tool,
313-
Handler: withToolTelemetry("mcp-add", handler),
314-
}
315290
}
316291

292+
// mcpAddTool implements a tool for adding new servers to the registry
293+
317294
// shortenURL creates a shortened URL using Bitly's API
318295
// It returns the shortened URL or an error if the request fails
319296
func shortenURL(ctx context.Context, longURL string) (string, error) {

pkg/gateway/reload.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,12 @@ func (g *Gateway) reloadConfiguration(ctx context.Context, configuration Configu
123123
g.mcpServer.AddTool(mcpConfigSetTool.Tool, mcpConfigSetTool.Handler)
124124
g.toolRegistrations[mcpConfigSetTool.Tool.Name] = *mcpConfigSetTool
125125

126+
// Add mcp-create-profile tool
127+
log.Log(" > mcp-create-profile: tool for creating or updating profiles with current gateway state")
128+
mcpCreateProfileTool := g.createMcpCreateProfileTool(clientConfig)
129+
g.mcpServer.AddTool(mcpCreateProfileTool.Tool, mcpCreateProfileTool.Handler)
130+
g.toolRegistrations[mcpCreateProfileTool.Tool.Name] = *mcpCreateProfileTool
131+
126132
// Add find-tools tool only if embeddings client is configured
127133
if g.embeddingsClient != nil {
128134
log.Log(" > find-tools: AI-powered tool recommendation based on task description")

0 commit comments

Comments
 (0)