feat(studio): MCP-clients editor + federated entry rendering (v0.22.0)
The viral-bridge story now has a UI surface. Settings → MCP Clients lists every configured server, shows last- discovery health (green dot + tool count, red dot + error kind), and offers Add / Remove / Refresh. Add-server dialog: name (no dots), endpoint, optional api_key_env, optional notes. On submit the hub persists + re-runs discovery in one round-trip; the Store fills with synthetic `mcp.<name>.<tool>` entries the operator can see immediately. StoreItem now carries `kind` and `provider`. Federated entries route through a bridge — no install bundle to download — and Studio's install button renders accordingly (via the existing "Not installable" path; the Phase-2 follow-up adds an explicit "Configure bridge" call-to-action). Settings dialog grew to 520×640 with a SingleChildScrollView so the additional panels don't crowd. Channel pill, system-AI panel, MCP-clients panel all coexist without a redesign. Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
11b0ac97a1
commit
9ef32df31e
5 changed files with 471 additions and 8 deletions
|
|
@ -339,6 +339,55 @@ class HubService {
|
|||
);
|
||||
}
|
||||
|
||||
/// Snapshot of every configured MCP server.
|
||||
Future<List<McpClientInfo>> listMcpClients() async {
|
||||
final r = await _client.listMcpClients();
|
||||
return _mapMcpClients(r);
|
||||
}
|
||||
|
||||
/// Re-run discovery against every configured MCP server on
|
||||
/// the running daemon. Returns the same shape as listMcpClients.
|
||||
Future<List<McpClientInfo>> refreshMcpClients() async {
|
||||
final r = await _client.refreshMcpClients();
|
||||
return _mapMcpClients(r);
|
||||
}
|
||||
|
||||
Future<List<McpClientInfo>> addMcpClient({
|
||||
required String name,
|
||||
required String endpoint,
|
||||
String apiKeyEnv = '',
|
||||
String description = '',
|
||||
}) async {
|
||||
final r = await _client.addMcpClient(
|
||||
name: name,
|
||||
endpoint: endpoint,
|
||||
apiKeyEnv: apiKeyEnv,
|
||||
description: description,
|
||||
);
|
||||
return _mapMcpClients(r);
|
||||
}
|
||||
|
||||
Future<List<McpClientInfo>> removeMcpClient(String name) async {
|
||||
final r = await _client.removeMcpClient(name);
|
||||
return _mapMcpClients(r);
|
||||
}
|
||||
|
||||
List<McpClientInfo> _mapMcpClients(ListMcpClientsResponse r) {
|
||||
return r.clients
|
||||
.map(
|
||||
(s) => McpClientInfo(
|
||||
name: s.config.name,
|
||||
endpoint: s.config.endpoint,
|
||||
apiKeyEnv: s.config.apiKeyEnv,
|
||||
description: s.config.description,
|
||||
toolCount: s.toolCount,
|
||||
errorKind: s.errorKind,
|
||||
message: s.message,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Active channel + per-channel daemon status snapshot.
|
||||
Future<ChannelStatusSnapshot> channelStatus() async {
|
||||
final r = await _client.channelStatus();
|
||||
|
|
@ -394,6 +443,8 @@ class HubService {
|
|||
iconUrl: e.iconUrl,
|
||||
screenshotUrls: e.screenshotUrls,
|
||||
docsUrl: e.docsUrl,
|
||||
kind: e.kind,
|
||||
provider: e.provider,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
|
@ -924,6 +975,36 @@ class ChannelStatusSnapshot {
|
|||
});
|
||||
}
|
||||
|
||||
/// One configured MCP server plus the last-discovery report.
|
||||
/// Surfaced by Studio's MCP-clients editor in Settings.
|
||||
class McpClientInfo {
|
||||
final String name;
|
||||
final String endpoint;
|
||||
final String apiKeyEnv;
|
||||
final String description;
|
||||
/// Number of tools the last discovery returned. 0 before
|
||||
/// discovery has run or on failure.
|
||||
final int toolCount;
|
||||
/// Empty on success; otherwise one of:
|
||||
/// invalid_endpoint / not_implemented / network / http /
|
||||
/// server / parse / env_missing.
|
||||
final String errorKind;
|
||||
/// Human-readable diagnostic.
|
||||
final String message;
|
||||
|
||||
const McpClientInfo({
|
||||
required this.name,
|
||||
required this.endpoint,
|
||||
required this.apiKeyEnv,
|
||||
required this.description,
|
||||
required this.toolCount,
|
||||
required this.errorKind,
|
||||
required this.message,
|
||||
});
|
||||
|
||||
bool get healthy => errorKind.isEmpty && toolCount > 0;
|
||||
}
|
||||
|
||||
/// Filesystem paths the daemon owns. Studio's Doctor page
|
||||
/// surfaces these as "Open" buttons so Windows operators who
|
||||
/// never touch a shell can still inspect everything via the OS
|
||||
|
|
@ -1078,6 +1159,15 @@ class StoreItem {
|
|||
/// Explicit docs URL override. When empty, the hub falls back
|
||||
/// to the well-known raw-README paths off [repository].
|
||||
final String docsUrl;
|
||||
/// "native" (downloadable bundle) or "federated" (routes
|
||||
/// through a bridge — MCP, n8n, …). Empty falls back to
|
||||
/// "native".
|
||||
final String kind;
|
||||
/// Provider id for federated entries. Empty for native.
|
||||
/// E.g. "filesystem" for an `mcp.filesystem.read_file` tool.
|
||||
final String provider;
|
||||
|
||||
bool get isFederated => kind == 'federated';
|
||||
|
||||
const StoreItem({
|
||||
required this.name,
|
||||
|
|
@ -1098,5 +1188,7 @@ class StoreItem {
|
|||
required this.iconUrl,
|
||||
required this.screenshotUrls,
|
||||
required this.docsUrl,
|
||||
required this.kind,
|
||||
required this.provider,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue