diff --git a/MCP_UI.md b/MCP_UI.md index e59377f..adc2da6 100644 --- a/MCP_UI.md +++ b/MCP_UI.md @@ -210,35 +210,182 @@ Each UI resource follows the MCP-UI protocol structure: - `ui://app-detail/{org}/{name}/{version}` - Single app detail view - `ui://app-instances-dashboard` - Instance listing dashboard +## Configuration + +### How Clients Discover UI Support + +Tools that support UI rendering advertise this capability in their metadata via the `tools/list` response: + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/list", + "result": { + "tools": [ + { + "name": "list_apps", + "description": "List all Edge Connect applications matching the specified filter. Supports UI rendering when _meta.ui_standard is set to 'mcpui'.", + "_meta": { + "ui_support": "mcpui" + }, + "inputSchema": {...} + }, + { + "name": "show_app", + "description": "Retrieve a specific Edge Connect application by its key. Supports UI rendering when _meta.ui_standard is set to 'mcpui'.", + "_meta": { + "ui_support": "mcpui" + }, + "inputSchema": {...} + }, + { + "name": "list_app_instances", + "description": "List all Edge Connect application instances. Supports UI rendering when _meta.ui_standard is set to 'mcpui'.", + "_meta": { + "ui_support": "mcpui" + }, + "inputSchema": {...} + } + ] + } +} +``` + +The `_meta.ui_support` field indicates which UI standard(s) the tool supports. Clients can use this information to decide whether to request UI rendering when calling the tool. + +### UI Standard Flag (Per-Call Control) + +The server respects the client's UI rendering preferences via the `_meta.ui_standard` field in individual tool call requests. This allows **MCP clients** to control whether UI resources should be rendered on a **per-call basis**. + +**Available UI Standards**: +- `"mcpui"` - MCP-UI protocol (renders interactive HTML dashboards) +- `""` (empty string) or omitted - No UI rendering (text-only responses) +- Custom standards can be added in the future + +**How Clients Request UI Rendering**: + +Clients specify their UI preference in the `_meta` field of the `tools/call` request: + +```json +{ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/call", + "params": { + "name": "list_apps", + "arguments": { + "region": "EU" + }, + "_meta": { + "ui_standard": "mcpui" + } + } +} +``` + +**Examples**: + +**To enable MCP-UI rendering for a specific call**: +```json +{ + "method": "tools/call", + "params": { + "name": "list_apps", + "arguments": {...}, + "_meta": { + "ui_standard": "mcpui" + } + } +} +``` + +**To disable UI rendering** (text-only response): +```json +{ + "method": "tools/call", + "params": { + "name": "list_apps", + "arguments": {...} + // No _meta field, or _meta.ui_standard omitted/empty + } +} +``` + +**Server Implementation**: + +The server checks the client's `_meta.ui_standard` preference on each tool call: + +```go +// Extract client's UI standard preference from tool call _meta +func getUIStandardFromRequest(req *mcp.CallToolRequest) string { + if req.Params == nil || req.Params.Meta == nil { + return "" + } + uiStandard, ok := req.Params.Meta["ui_standard"] + // ... return the string value +} + +// Check if client wants UI rendering for this call +if !shouldRenderUI(req, "mcpui") { + // Return text-only response + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{Text: result}}, + }, nil, nil +} + +// ... proceed with UI generation +``` + +**Default Behavior**: +- If the client does not include `_meta.ui_standard` in the tool call, the server returns text-only responses +- The three tools (`list_apps`, `show_app`, `list_app_instances`) support UI rendering when requested +- If UI rendering fails, the tools gracefully fall back to text-only responses +- The `shouldRenderUI()` helper function checks the call's `_meta` field before attempting UI generation + +**Benefits**: +- **Per-Call Control**: Clients decide on each call whether they want UI rendering +- **No Configuration Required**: Clients don't need to set preferences during initialization +- **Flexibility**: Support multiple UI standards in the future +- **Discovery**: Clients learn about UI support from `tools/list` response +- **Backward Compatibility**: Existing clients without UI support work seamlessly +- **Graceful Degradation**: Always provides text-based fallback +- **Performance**: Avoid UI generation overhead when clients don't request it + ## Usage ### Prerequisites 1. **MCP Client Support**: Your MCP client must support UI resources (embedded resources in tool responses) 2. **Edge Connect Configuration**: Server must be properly configured with Edge Connect credentials +3. **Client UI Capability**: Client must include `_meta.ui_standard` field in tool call requests to receive UI resources ### Example Workflow -1. **List Applications**: -```bash -# User asks: "List all Edge Connect applications" -# Server executes list_apps tool -# Returns: Text summary + Interactive HTML dashboard -``` +1. **Client Discovers Tools**: + - Client sends `tools/list` request + - Server responds with tool list including `_meta.ui_support` field + - Client sees that `list_apps`, `show_app`, and `list_app_instances` support `"mcpui"` -2. **View Application Details**: -```bash -# User asks: "Show me details for app myorg/myapp:1.0.0" -# Server executes show_app tool -# Returns: JSON data + Interactive detail view -``` +2. **List Applications with UI**: + - User asks: "List all Edge Connect applications" + - Client calls `list_apps` with `_meta.ui_standard: "mcpui"` + - Server returns: Text summary + Interactive HTML dashboard -3. **List Instances**: -```bash -# User asks: "Show all application instances" -# Server executes list_app_instances tool -# Returns: Text summary + Interactive dashboard -``` +3. **View Application Details with UI**: + - User asks: "Show me details for app myorg/myapp:1.0.0" + - Client calls `show_app` with `_meta.ui_standard: "mcpui"` + - Server returns: JSON data + Interactive detail view + +4. **List Instances without UI**: + - User asks: "Show all application instances as text" + - Client calls `list_app_instances` without `_meta.ui_standard` + - Server returns: Text-only response (no UI) + +**Key Points**: +- Each tool call can independently request UI rendering or not +- Clients that don't support UI simply omit the `_meta.ui_standard` field +- Clients can mix UI and non-UI calls based on context or user preferences ### Graceful Degradation diff --git a/README.md b/README.md index ac3103a..bfc3373 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,14 @@ This server includes **rich, interactive web-based visualizations** powered by [ - **🔍 Application Detail View** - Comprehensive property display with JSON viewer - **⚡ Instances Dashboard** - Interactive table with status indicators and instance management -For clients that don't support UI resources, the server gracefully falls back to text-based responses. See [MCP_UI.md](./MCP_UI.md) for full documentation. +**UI Standard Support**: The server respects client preferences for UI rendering on a per-call basis via the `_meta.ui_standard` field: +- Clients include `"_meta": {"ui_standard": "mcpui"}` in `tools/call` requests to receive MCP-UI interactive dashboards +- Clients omit `_meta.ui_standard` or set it to `""` to receive text-only responses +- Tools advertise UI support via `_meta.ui_support` field in `tools/list` responses +- Server supports multiple UI standards for future extensibility +- This gives clients full control on each call whether they want to render UI resources + +For clients that don't support UI resources, the server gracefully falls back to text-based responses. See [MCP_UI.md](./MCP_UI.md) for full documentation and configuration details. ### Edge Connect API Tools diff --git a/tools.go b/tools.go index 71d419d..82368a3 100644 --- a/tools.go +++ b/tools.go @@ -117,6 +117,40 @@ func getProtocolFromRequest(req *mcp.CallToolRequest) mcpuiserver.ProtocolType { return mcpuiserver.ParseProtocolFromInitialize(paramsMap) } +// getUIStandardFromRequest extracts the ui_standard value from the tool call's _meta field +// Returns empty string if not set or not a string +func getUIStandardFromRequest(req *mcp.CallToolRequest) string { + // Get _meta from the tool call params + if req.Params == nil || req.Params.Meta == nil { + return "" + } + + // Look for ui_standard in the _meta field + uiStandard, ok := req.Params.Meta["ui_standard"] + if !ok { + return "" + } + + uiStandardStr, ok := uiStandard.(string) + if !ok { + return "" + } + + return uiStandardStr +} + +// shouldRenderUI checks if UI rendering should be performed based on client's tool call _meta +// Returns true if client's ui_standard is set to the requested standard (e.g., "mcpui") +func shouldRenderUI(req *mcp.CallToolRequest, standard string) bool { + uiStandard := getUIStandardFromRequest(req) + if uiStandard == "" { + return false // No UI standard specified by client, don't render + } + + // Check if the client's UI standard matches the requested standard + return uiStandard == standard +} + // filterAppInstances performs client-side partial matching on app instances func filterAppInstances(instances []v2.AppInstance, organization, instanceName, cloudletOrg, cloudletName, appOrg, appName, appVersion *string) []v2.AppInstance { if organization == nil && instanceName == nil && @@ -324,7 +358,10 @@ func registerShowAppTool(s *mcp.Server) { mcp.AddTool(s, &mcp.Tool{ Name: "show_app", - Description: "Retrieve a specific Edge Connect application by its key.", + Description: "Retrieve a specific Edge Connect application by its key. Supports UI rendering when _meta.ui_standard is set to 'mcpui'.", + Meta: mcp.Meta{ + "ui_support": "mcpui", + }, }, func(ctx context.Context, req *mcp.CallToolRequest, a args) (*mcp.CallToolResult, any, error) { region := config.DefaultRegion if a.Region != nil { @@ -347,6 +384,14 @@ func registerShowAppTool(s *mcp.Server) { return nil, nil, fmt.Errorf("failed to serialize app: %w", err) } + // Check if UI rendering is enabled by the client + if !shouldRenderUI(req, "mcpui") { + // UI rendering is disabled, return text-only response + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{Text: string(appJSON)}}, + }, nil, nil + } + // Parse protocol from MCP initialize request protocol := getProtocolFromRequest(req) @@ -388,7 +433,10 @@ func registerListAppsTool(s *mcp.Server) { mcp.AddTool(s, &mcp.Tool{ Name: "list_apps", - Description: "List all Edge Connect applications matching the specified filter. Supports partial (substring) matching for all filter fields.", + Description: "List all Edge Connect applications matching the specified filter. Supports partial (substring) matching for all filter fields. Supports UI rendering when _meta.ui_standard is set to 'mcpui'.", + Meta: mcp.Meta{ + "ui_support": "mcpui", + }, }, func(ctx context.Context, req *mcp.CallToolRequest, a args) (*mcp.CallToolResult, any, error) { region := config.DefaultRegion if a.Region != nil { @@ -417,6 +465,16 @@ func registerListAppsTool(s *mcp.Server) { return nil, nil, fmt.Errorf("failed to serialize apps: %w", err) } + result := fmt.Sprintf("Found %d apps:\n%s", len(apps), string(appsJSON)) + + // Check if UI rendering is enabled by the client + if !shouldRenderUI(req, "mcpui") { + // UI rendering is disabled, return text-only response + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{Text: result}}, + }, nil, nil + } + // Parse protocol from MCP initialize request protocol := getProtocolFromRequest(req) @@ -424,14 +482,11 @@ func registerListAppsTool(s *mcp.Server) { uiResource, err := createAppListUI(apps, region, protocol) if err != nil { // If UI creation fails, just return text content - result := fmt.Sprintf("Found %d apps:\n%s", len(apps), string(appsJSON)) return &mcp.CallToolResult{ Content: []mcp.Content{&mcp.TextContent{Text: result}}, }, nil, nil } - result := fmt.Sprintf("Found %d apps:\n%s", len(apps), string(appsJSON)) - // Convert UIResource to MCP EmbeddedResource resourceContents := &mcp.ResourceContents{ URI: uiResource.Resource.URI, @@ -745,7 +800,10 @@ func registerListAppInstancesTool(s *mcp.Server) { mcp.AddTool(s, &mcp.Tool{ Name: "list_app_instances", - Description: "List all Edge Connect application instances matching the specified filter. Supports partial (substring) matching for all filter fields.", + Description: "List all Edge Connect application instances matching the specified filter. Supports partial (substring) matching for all filter fields. Supports UI rendering when _meta.ui_standard is set to 'mcpui'.", + Meta: mcp.Meta{ + "ui_support": "mcpui", + }, }, func(ctx context.Context, req *mcp.CallToolRequest, a args) (*mcp.CallToolResult, any, error) { region := config.DefaultRegion if a.Region != nil { @@ -783,6 +841,16 @@ func registerListAppInstancesTool(s *mcp.Server) { return nil, nil, fmt.Errorf("failed to serialize app instances: %w", err) } + result := fmt.Sprintf("Found %d app instances:\n%s", len(appInsts), string(appInstsJSON)) + + // Check if UI rendering is enabled by the client + if !shouldRenderUI(req, "mcpui") { + // UI rendering is disabled, return text-only response + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{Text: result}}, + }, nil, nil + } + // Parse protocol from MCP initialize request protocol := getProtocolFromRequest(req) @@ -790,14 +858,11 @@ func registerListAppInstancesTool(s *mcp.Server) { uiResource, err := createAppInstanceListUI(appInsts, region, protocol) if err != nil { // If UI creation fails, just return text content - result := fmt.Sprintf("Found %d app instances:\n%s", len(appInsts), string(appInstsJSON)) return &mcp.CallToolResult{ Content: []mcp.Content{&mcp.TextContent{Text: result}}, }, nil, nil } - result := fmt.Sprintf("Found %d app instances:\n%s", len(appInsts), string(appInstsJSON)) - // Convert UIResource to MCP EmbeddedResource resourceContents := &mcp.ResourceContents{ URI: uiResource.Resource.URI,