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.
|
/// Active channel + per-channel daemon status snapshot.
|
||||||
Future<ChannelStatusSnapshot> channelStatus() async {
|
Future<ChannelStatusSnapshot> channelStatus() async {
|
||||||
final r = await _client.channelStatus();
|
final r = await _client.channelStatus();
|
||||||
|
|
@ -394,6 +443,8 @@ class HubService {
|
||||||
iconUrl: e.iconUrl,
|
iconUrl: e.iconUrl,
|
||||||
screenshotUrls: e.screenshotUrls,
|
screenshotUrls: e.screenshotUrls,
|
||||||
docsUrl: e.docsUrl,
|
docsUrl: e.docsUrl,
|
||||||
|
kind: e.kind,
|
||||||
|
provider: e.provider,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList();
|
.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
|
/// Filesystem paths the daemon owns. Studio's Doctor page
|
||||||
/// surfaces these as "Open" buttons so Windows operators who
|
/// surfaces these as "Open" buttons so Windows operators who
|
||||||
/// never touch a shell can still inspect everything via the OS
|
/// 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
|
/// Explicit docs URL override. When empty, the hub falls back
|
||||||
/// to the well-known raw-README paths off [repository].
|
/// to the well-known raw-README paths off [repository].
|
||||||
final String docsUrl;
|
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({
|
const StoreItem({
|
||||||
required this.name,
|
required this.name,
|
||||||
|
|
@ -1098,5 +1188,7 @@ class StoreItem {
|
||||||
required this.iconUrl,
|
required this.iconUrl,
|
||||||
required this.screenshotUrls,
|
required this.screenshotUrls,
|
||||||
required this.docsUrl,
|
required this.docsUrl,
|
||||||
|
required this.kind,
|
||||||
|
required this.provider,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import 'widgets/widgets.dart';
|
||||||
/// Studio's own build version. Bump on every UI commit so the
|
/// Studio's own build version. Bump on every UI commit so the
|
||||||
/// running app self-identifies — visible in the sidebar header
|
/// running app self-identifies — visible in the sidebar header
|
||||||
/// and quick-glance proof that you're seeing the current build.
|
/// and quick-glance proof that you're seeing the current build.
|
||||||
const String kStudioVersion = '0.21.0';
|
const String kStudioVersion = '0.22.0';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
||||||
String? _channelToast;
|
String? _channelToast;
|
||||||
ChannelStatusSnapshot? _channels;
|
ChannelStatusSnapshot? _channels;
|
||||||
SystemAiStatus? _aiStatus;
|
SystemAiStatus? _aiStatus;
|
||||||
|
List<McpClientInfo>? _mcpClients;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -47,6 +48,64 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
||||||
_secure = ep.secure;
|
_secure = ep.secure;
|
||||||
_loadChannels();
|
_loadChannels();
|
||||||
_loadAiStatus();
|
_loadAiStatus();
|
||||||
|
_loadMcpClients();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadMcpClients() async {
|
||||||
|
try {
|
||||||
|
final list = await HubService.instance.listMcpClients();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _mcpClients = list);
|
||||||
|
} catch (_) {
|
||||||
|
// Same fail-quiet rule as channels.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _addMcpClient(McpClientDraft draft) async {
|
||||||
|
try {
|
||||||
|
final list = await HubService.instance.addMcpClient(
|
||||||
|
name: draft.name,
|
||||||
|
endpoint: draft.endpoint,
|
||||||
|
apiKeyEnv: draft.apiKeyEnv,
|
||||||
|
description: draft.description,
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _mcpClients = list);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Added MCP server "${draft.name}".')),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Add failed: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _removeMcpClient(String name) async {
|
||||||
|
try {
|
||||||
|
final list = await HubService.instance.removeMcpClient(name);
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _mcpClients = list);
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Remove failed: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _refreshMcpClients() async {
|
||||||
|
try {
|
||||||
|
final list = await HubService.instance.refreshMcpClients();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _mcpClients = list);
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Refresh failed: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadChannels() async {
|
Future<void> _loadChannels() async {
|
||||||
|
|
@ -164,11 +223,12 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
||||||
vertical: FaiSpace.lg,
|
vertical: FaiSpace.lg,
|
||||||
),
|
),
|
||||||
content: ConstrainedBox(
|
content: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 380),
|
constraints: const BoxConstraints(maxWidth: 520, maxHeight: 640),
|
||||||
child: Column(
|
child: SingleChildScrollView(
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Where should Studio connect? Default is the local hub at 127.0.0.1:50051.',
|
'Where should Studio connect? Default is the local hub at 127.0.0.1:50051.',
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
|
@ -322,8 +382,18 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
if (_mcpClients != null) ...[
|
||||||
|
const SizedBox(height: FaiSpace.lg),
|
||||||
|
_McpClientsPanel(
|
||||||
|
clients: _mcpClients!,
|
||||||
|
onAdd: _addMcpClient,
|
||||||
|
onRemove: _removeMcpClient,
|
||||||
|
onRefresh: _refreshMcpClients,
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
|
|
@ -611,3 +681,304 @@ class _StatRow extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Draft passed back from the add-server dialog. Kept tiny so
|
||||||
|
/// the panel can stay stateless.
|
||||||
|
class McpClientDraft {
|
||||||
|
final String name;
|
||||||
|
final String endpoint;
|
||||||
|
final String apiKeyEnv;
|
||||||
|
final String description;
|
||||||
|
const McpClientDraft({
|
||||||
|
required this.name,
|
||||||
|
required this.endpoint,
|
||||||
|
required this.apiKeyEnv,
|
||||||
|
required this.description,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MCP-clients panel — shows every configured server with its
|
||||||
|
/// last-discovery health, plus Add / Remove / Refresh actions.
|
||||||
|
/// This is *the* surface that wires the viral-bridge story to
|
||||||
|
/// the operator: configure a server here, watch the store
|
||||||
|
/// fill with synthetic capabilities. Mirrors `fai mcp …` on
|
||||||
|
/// the CLI.
|
||||||
|
class _McpClientsPanel extends StatelessWidget {
|
||||||
|
final List<McpClientInfo> clients;
|
||||||
|
final ValueChanged<McpClientDraft> onAdd;
|
||||||
|
final ValueChanged<String> onRemove;
|
||||||
|
final VoidCallback onRefresh;
|
||||||
|
|
||||||
|
const _McpClientsPanel({
|
||||||
|
required this.clients,
|
||||||
|
required this.onAdd,
|
||||||
|
required this.onRemove,
|
||||||
|
required this.onRefresh,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'MCP CLIENTS',
|
||||||
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
letterSpacing: 0.6,
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: FaiSpace.sm),
|
||||||
|
FaiPill(
|
||||||
|
label: '${clients.length} server${clients.length == 1 ? "" : "s"}',
|
||||||
|
tone: FaiPillTone.neutral,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.refresh, size: 16),
|
||||||
|
tooltip: 'Re-run discovery',
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
onPressed: onRefresh,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.add, size: 16),
|
||||||
|
tooltip: 'Add MCP server',
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
onPressed: () async {
|
||||||
|
final draft = await _AddMcpClientDialog.show(context);
|
||||||
|
if (draft != null) onAdd(draft);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
'External MCP servers federate their tools as `mcp.<server>.<tool>` capabilities. '
|
||||||
|
'Configure once, see them in the Store.',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: FaiSpace.sm),
|
||||||
|
if (clients.isEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: FaiSpace.sm),
|
||||||
|
child: Text(
|
||||||
|
'No MCP servers configured. Click + to add one.',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
for (final c in clients)
|
||||||
|
_McpClientRow(
|
||||||
|
client: c,
|
||||||
|
onRemove: () => onRemove(c.name),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _McpClientRow extends StatelessWidget {
|
||||||
|
final McpClientInfo client;
|
||||||
|
final VoidCallback onRemove;
|
||||||
|
const _McpClientRow({required this.client, required this.onRemove});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final ok = client.healthy;
|
||||||
|
final dotColor = client.errorKind.isEmpty
|
||||||
|
? (ok ? FaiColors.success : FaiColors.muted)
|
||||||
|
: theme.colorScheme.error;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 6),
|
||||||
|
child: Icon(Icons.circle, size: 8, color: dotColor),
|
||||||
|
),
|
||||||
|
const SizedBox(width: FaiSpace.sm),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
client.name,
|
||||||
|
style: FaiTheme.mono(
|
||||||
|
size: 12,
|
||||||
|
weight: FontWeight.w600,
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: FaiSpace.sm),
|
||||||
|
if (client.toolCount > 0)
|
||||||
|
FaiPill(
|
||||||
|
label: '${client.toolCount} tools',
|
||||||
|
tone: FaiPillTone.success,
|
||||||
|
)
|
||||||
|
else if (client.errorKind.isNotEmpty)
|
||||||
|
FaiPill(
|
||||||
|
label: client.errorKind,
|
||||||
|
tone: FaiPillTone.danger,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
client.endpoint,
|
||||||
|
style: FaiTheme.mono(
|
||||||
|
size: 10,
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (client.message.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
client.message,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: client.errorKind.isEmpty
|
||||||
|
? theme.colorScheme.onSurfaceVariant
|
||||||
|
: theme.colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.delete_outline, size: 14),
|
||||||
|
tooltip: 'Remove server',
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
onPressed: onRemove,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AddMcpClientDialog extends StatefulWidget {
|
||||||
|
const _AddMcpClientDialog();
|
||||||
|
|
||||||
|
static Future<McpClientDraft?> show(BuildContext context) {
|
||||||
|
return showDialog<McpClientDraft>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const _AddMcpClientDialog(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_AddMcpClientDialog> createState() => _AddMcpClientDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AddMcpClientDialogState extends State<_AddMcpClientDialog> {
|
||||||
|
final _name = TextEditingController();
|
||||||
|
final _endpoint = TextEditingController();
|
||||||
|
final _apiKey = TextEditingController();
|
||||||
|
final _desc = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_name.dispose();
|
||||||
|
_endpoint.dispose();
|
||||||
|
_apiKey.dispose();
|
||||||
|
_desc.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Add MCP server'),
|
||||||
|
content: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 480),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Tools the server advertises via `tools/list` appear in the Store as '
|
||||||
|
'`mcp.<name>.<tool>` capabilities.',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: FaiSpace.md),
|
||||||
|
TextField(
|
||||||
|
controller: _name,
|
||||||
|
autofocus: true,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Name (no dots)',
|
||||||
|
hintText: 'filesystem',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
isDense: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: FaiSpace.sm),
|
||||||
|
TextField(
|
||||||
|
controller: _endpoint,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Endpoint',
|
||||||
|
hintText: 'http://127.0.0.1:3001/mcp',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
isDense: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: FaiSpace.sm),
|
||||||
|
TextField(
|
||||||
|
controller: _apiKey,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'API key env var (optional)',
|
||||||
|
hintText: 'MCP_FILESYSTEM_TOKEN',
|
||||||
|
prefixText: '\$',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
isDense: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: FaiSpace.sm),
|
||||||
|
TextField(
|
||||||
|
controller: _desc,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Notes (optional)',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
isDense: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, null),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (_name.text.trim().isEmpty || _endpoint.text.trim().isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Navigator.pop(
|
||||||
|
context,
|
||||||
|
McpClientDraft(
|
||||||
|
name: _name.text.trim(),
|
||||||
|
endpoint: _endpoint.text.trim(),
|
||||||
|
apiKeyEnv: _apiKey.text.trim(),
|
||||||
|
description: _desc.text.trim(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Text('Add + discover'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ packages:
|
||||||
path: "../fai_dart_sdk"
|
path: "../fai_dart_sdk"
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "0.11.0"
|
version: "0.12.1"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
name: fai_studio
|
name: fai_studio
|
||||||
description: "F∆I Studio — desktop GUI for the F∆I hub"
|
description: "F∆I Studio — desktop GUI for the F∆I hub"
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 0.21.0
|
version: 0.22.0
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.0-200.1.beta
|
sdk: ^3.11.0-200.1.beta
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue