Offer the development server and the browser to LLM nodes

A flow could write software but never run it: the coding agent edits files and
that is where it ended. These two entries close the loop - start the code the
execution wrote, then open it in a browser and read what the page says.

Both are streamable-http, so an LLM node binds them with no new Java: the
retriever already filtered the catalogue by transport and the draft normalizer
already dropped unknown servers. The development server's preview is pinned to
the execution with x-preview-key, the way the coding agent's workspace already
is, so the scope travels in a header the service fills in rather than in an
argument a model could choose for itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lucio Lelii 2026-09-22 10:29:37 +02:00
parent 51651a2013
commit 653cc0f6d6
2 changed files with 94 additions and 0 deletions

View File

@ -122,6 +122,83 @@
}
}
}
},
{
"id": "dev-server-mcp",
"name": "Development Server MCP Server",
"description": "Starts and observes preconfigured development services. One instance per execution, in a throwaway copy of that execution's workspace, each on its own port. The caller chooses a service id and nothing else: commands, arguments and directories belong to the operator.",
"transport": "streamable-http",
"url": "${{host}}/dev-server/mcp",
"headers": {
"Authorization": "Bearer ${{key}}",
"x-preview-key": "${{context.rootExecutionId}}"
},
"configurationSchema": {
"type": "object",
"additionalProperties": false,
"required": ["host", "key"],
"properties": {
"host": {
"type": "string",
"title": "Host",
"description": "Base host for the development server MCP endpoint, including scheme and without a trailing slash.",
"examples": ["https://host.it"],
"minLength": 1
},
"key": {
"type": "string",
"title": "API Key",
"description": "Bearer token used to authorize requests to the development server MCP server.",
"format": "password",
"writeOnly": true,
"minLength": 1,
"x-ui-bindable-as-input": true,
"x-ui-binding-static-label": "static value",
"x-ui-binding-input-label": "node input",
"x-ui-input-name": "key",
"x-ui-input-type": "TEXT",
"x-ui-input-multiple": false
}
}
}
},
{
"id": "browser-mcp",
"name": "Browser MCP Server",
"description": "Drives a headless browser against the origins its operator allows, and reports what the page says: text snapshots, console messages and network errors. Screenshots are taken but a text-only caller receives a placeholder, not the image.",
"transport": "streamable-http",
"url": "${{host}}/browser/mcp",
"headers": {
"Authorization": "Bearer ${{key}}"
},
"configurationSchema": {
"type": "object",
"additionalProperties": false,
"required": ["host", "key"],
"properties": {
"host": {
"type": "string",
"title": "Host",
"description": "Base host for the browser MCP endpoint, including scheme and without a trailing slash.",
"examples": ["https://host.it"],
"minLength": 1
},
"key": {
"type": "string",
"title": "API Key",
"description": "Bearer token used to authorize requests to the browser MCP server.",
"format": "password",
"writeOnly": true,
"minLength": 1,
"x-ui-bindable-as-input": true,
"x-ui-binding-static-label": "static value",
"x-ui-binding-input-label": "node input",
"x-ui-input-name": "key",
"x-ui-input-type": "TEXT",
"x-ui-input-multiple": false
}
}
}
}
]
}

View File

@ -49,10 +49,27 @@ public class RetrieverControllerTest {
List<String> values = retrieverController.getValues("MCPServers", "streamableHttpServers", Map.of());
assertTrue(values.contains("coding-agent-mcp"));
// The three that make a development loop: write the code, run it, look at it in a browser.
assertTrue(values.contains("dev-server-mcp"));
assertTrue(values.contains("browser-mcp"));
assertFalse(values.contains("postgres"));
assertFalse(values.contains("url-downloader"));
}
@Test
public void pinsTheDevServerPreviewToTheExecutionThatAskedForIt() {
// The key names a scope the model may not choose for itself, so it travels as a header the
// service fills in - the same way the coding agent's workspace is pinned. A tool argument
// would let one execution ask for another's preview by name.
MCPServersProvider.MCPServerDefinition devServer = mcpServersProvider.getServers().stream()
.filter(server -> "dev-server-mcp".equals(server.id()))
.findFirst()
.orElseThrow();
assertEquals("${{context.rootExecutionId}}", devServer.headers().get("x-preview-key"));
assertEquals("streamable-http", devServer.transport());
}
@Test
public void llmModelsAreOpenForAProviderThatCannotListThem() {
// Gemini's catalogue is not listable without a credential and its names move constantly, so