feat(studio): per-field ports in flow editor + store-doc scroll fix
Some checks failed
Security / Security check (push) Failing after 1s
Some checks failed
Security / Security check (push) Failing after 1s
Picks up: - fai_studio_flow_editor 0.9.0: per-field input + output ports rendered from ModuleInfo, i18n tooltips, four additional canvas patterns ported from jai_client. - fai_client_sdk 0.18.0: regenerated ModuleField bindings for the schema_version 3 / per-field-description proto bump in fai/platform. Wires StudioFlowRunDriver.moduleInfo so the editor can fetch declared field info per step capability. The hub maps schema_version 3 manifest fields (inputs/outputs with description.en/de) through ModuleInfoResponse; this patch wraps them back into the editor's ModuleSpec / ModuleField vocabulary so the canvas paints them as distinct anchors with hover tooltips. ModuleDetail in hub.dart now carries the same inputs + outputs lists (with ModuleFieldInfo type + i18n description map), enabling other Studio surfaces — e.g. the module-detail sheet — to render bilingual field docs in a later pass. Plus: store-doc nested-scroll fix. The module detail panel's docs section sat inside its own 480px viewport inside the outer sheet's SingleChildScrollView, producing the double-scrollbar feel. Setting physics: NeverScrollableScrollPhysics on the Markdown widget and dropping the maxHeight lets the docs scroll as part of the outer sheet — one scrollbar, predictable mouse-wheel behaviour. Bumps fai_studio to 0.56.0. Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
34d086c29b
commit
a698be0b57
5 changed files with 466 additions and 336 deletions
|
|
@ -133,12 +133,9 @@ class HubService {
|
|||
return ModuleSummary(
|
||||
name: e.key,
|
||||
version: caps.first.moduleVersion,
|
||||
capabilities: caps
|
||||
.map((c) => '${c.capability}@${c.version}')
|
||||
.toList(),
|
||||
capabilities: caps.map((c) => '${c.capability}@${c.version}').toList(),
|
||||
);
|
||||
}).toList()
|
||||
..sort((a, b) => a.name.compareTo(b.name));
|
||||
}).toList()..sort((a, b) => a.name.compareTo(b.name));
|
||||
}
|
||||
|
||||
/// Every capability the hub can execute, with the `kind` tag
|
||||
|
|
@ -151,15 +148,17 @@ class HubService {
|
|||
Future<List<CapabilityInfo>> allCapabilities() async {
|
||||
final caps = await _client.listCapabilities();
|
||||
return caps
|
||||
.map((c) => CapabilityInfo(
|
||||
capability: c.capability,
|
||||
version: c.version,
|
||||
moduleName: c.moduleName,
|
||||
kind: c.kind.isEmpty ? 'wasm' : c.kind,
|
||||
provider: c.provider,
|
||||
sourceKind: c.sourceKind,
|
||||
versionAdvisory: c.versionAdvisory,
|
||||
))
|
||||
.map(
|
||||
(c) => CapabilityInfo(
|
||||
capability: c.capability,
|
||||
version: c.version,
|
||||
moduleName: c.moduleName,
|
||||
kind: c.kind.isEmpty ? 'wasm' : c.kind,
|
||||
provider: c.provider,
|
||||
sourceKind: c.sourceKind,
|
||||
versionAdvisory: c.versionAdvisory,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
|
@ -175,6 +174,24 @@ class HubService {
|
|||
permissions: r.permissions,
|
||||
directory: r.directory,
|
||||
acceptsMime: r.acceptsMime,
|
||||
inputs: r.inputs
|
||||
.map(
|
||||
(f) => ModuleFieldInfo(
|
||||
name: f.name,
|
||||
type: f.type,
|
||||
description: Map<String, String>.from(f.description),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
outputs: r.outputs
|
||||
.map(
|
||||
(f) => ModuleFieldInfo(
|
||||
name: f.name,
|
||||
type: f.type,
|
||||
description: Map<String, String>.from(f.description),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -280,7 +297,7 @@ class HubService {
|
|||
/// Studio's editor uses this to populate a dropdown instead
|
||||
/// of forcing the operator to type the id manually.
|
||||
Future<({String errorKind, String text, List<String> ids})>
|
||||
listSystemAiModels({
|
||||
listSystemAiModels({
|
||||
String provider = '',
|
||||
String endpoint = '',
|
||||
String apiKeyEnv = '',
|
||||
|
|
@ -300,8 +317,7 @@ class HubService {
|
|||
/// Pull (download) an Ollama model via /api/pull.
|
||||
/// Synchronous; can take minutes for large models. Errors
|
||||
/// come back as `errorKind`.
|
||||
Future<({String errorKind, String text, int elapsedMs})>
|
||||
pullSystemAiModel({
|
||||
Future<({String errorKind, String text, int elapsedMs})> pullSystemAiModel({
|
||||
required String endpoint,
|
||||
required String model,
|
||||
String apiKeyEnv = '',
|
||||
|
|
@ -311,11 +327,7 @@ class HubService {
|
|||
model: model,
|
||||
apiKeyEnv: apiKeyEnv,
|
||||
);
|
||||
return (
|
||||
errorKind: r.errorKind,
|
||||
text: r.text,
|
||||
elapsedMs: r.elapsedMs,
|
||||
);
|
||||
return (errorKind: r.errorKind, text: r.text, elapsedMs: r.elapsedMs);
|
||||
}
|
||||
|
||||
/// Detected host hardware. Used by the System-AI editor to
|
||||
|
|
@ -368,10 +380,7 @@ class HubService {
|
|||
required String reviewer,
|
||||
required String reason,
|
||||
}) async {
|
||||
final r = await _client.clearEventLog(
|
||||
reviewer: reviewer,
|
||||
reason: reason,
|
||||
);
|
||||
final r = await _client.clearEventLog(reviewer: reviewer, reason: reason);
|
||||
return (purged: r.purged.toInt(), channel: r.channel);
|
||||
}
|
||||
|
||||
|
|
@ -407,15 +416,16 @@ class HubService {
|
|||
/// Read the operator's `default_scope:` plus the catalog-known
|
||||
/// publisher shortlist. Used by Settings → DEFAULT SCOPE panel.
|
||||
Future<({List<String> scope, List<String> knownProviders})>
|
||||
getDefaultScope() async {
|
||||
getDefaultScope() async {
|
||||
return _client.getDefaultScope();
|
||||
}
|
||||
|
||||
/// Replace the operator's `default_scope:` and hot-swap on the
|
||||
/// running hub. Caller passes the new ordered list; hub rejects
|
||||
/// an empty list with InvalidArgument.
|
||||
Future<({List<String> scope, List<String> knownProviders})>
|
||||
setDefaultScope(List<String> scope) async {
|
||||
Future<({List<String> scope, List<String> knownProviders})> setDefaultScope(
|
||||
List<String> scope,
|
||||
) async {
|
||||
return _client.setDefaultScope(scope);
|
||||
}
|
||||
|
||||
|
|
@ -425,13 +435,14 @@ class HubService {
|
|||
/// is installed side-by-side.
|
||||
Future<List<String>> installedVersions(String moduleName) async {
|
||||
final caps = await _client.listCapabilities();
|
||||
final versions = caps
|
||||
.where((c) => c.moduleName == moduleName)
|
||||
.where((c) => c.kind.isEmpty || c.kind == 'wasm')
|
||||
.map((c) => c.moduleVersion)
|
||||
.toSet()
|
||||
.toList()
|
||||
..sort();
|
||||
final versions =
|
||||
caps
|
||||
.where((c) => c.moduleName == moduleName)
|
||||
.where((c) => c.kind.isEmpty || c.kind == 'wasm')
|
||||
.map((c) => c.moduleVersion)
|
||||
.toSet()
|
||||
.toList()
|
||||
..sort();
|
||||
return versions;
|
||||
}
|
||||
|
||||
|
|
@ -444,7 +455,7 @@ class HubService {
|
|||
/// - `'no_docs'` — module installed but bundle had no docs
|
||||
/// - `'not_installed'` — module isn't installed at all
|
||||
Future<({String state, String text, String sourcePath})>
|
||||
readInstalledModuleDocs(String name, {String locale = ''}) async {
|
||||
readInstalledModuleDocs(String name, {String locale = ''}) async {
|
||||
final r = await _client.getInstalledModuleDocs(name, locale: locale);
|
||||
final String state;
|
||||
if (r.notInstalled) {
|
||||
|
|
@ -742,7 +753,9 @@ class HubService {
|
|||
int backfill = 0,
|
||||
List<String> types = const [],
|
||||
}) {
|
||||
return _client.streamEvents(backfill: backfill, types: types).map(
|
||||
return _client
|
||||
.streamEvents(backfill: backfill, types: types)
|
||||
.map(
|
||||
(e) => AuditEvent(
|
||||
eventId: e.eventId,
|
||||
timestamp: DateTime.tryParse(e.timestamp) ?? DateTime.now(),
|
||||
|
|
@ -774,12 +787,9 @@ class HubService {
|
|||
flowName: e.flowName.isEmpty ? null : e.flowName,
|
||||
stepId: e.stepId.isEmpty ? null : e.stepId,
|
||||
moduleName: e.moduleName.isEmpty ? null : e.moduleName,
|
||||
moduleVersion:
|
||||
e.moduleVersion.isEmpty ? null : e.moduleVersion,
|
||||
invocationId:
|
||||
e.invocationId.isEmpty ? null : e.invocationId,
|
||||
flowExecution:
|
||||
e.flowExecution.isEmpty ? null : e.flowExecution,
|
||||
moduleVersion: e.moduleVersion.isEmpty ? null : e.moduleVersion,
|
||||
invocationId: e.invocationId.isEmpty ? null : e.invocationId,
|
||||
flowExecution: e.flowExecution.isEmpty ? null : e.flowExecution,
|
||||
durationMs: e.durationMs == 0 ? null : e.durationMs.toInt(),
|
||||
error: e.error.isEmpty ? null : e.error,
|
||||
detail: e.detail.isEmpty ? null : e.detail,
|
||||
|
|
@ -826,8 +836,7 @@ class HubService {
|
|||
flowName: e.flowName,
|
||||
stepId: e.stepId,
|
||||
prompt: e.prompt,
|
||||
payloadPreview:
|
||||
e.payloadPreview.isEmpty ? null : e.payloadPreview,
|
||||
payloadPreview: e.payloadPreview.isEmpty ? null : e.payloadPreview,
|
||||
createdAt: DateTime.tryParse(e.createdAt) ?? DateTime.now(),
|
||||
expiresAt: e.expiresAt.isEmpty
|
||||
? null
|
||||
|
|
@ -847,26 +856,19 @@ class HubService {
|
|||
_client.approve(approvalId: id, reviewer: reviewer);
|
||||
|
||||
Future<void> reject(String id, String reviewer, String reason) =>
|
||||
_client.reject(
|
||||
approvalId: id,
|
||||
reviewer: reviewer,
|
||||
reason: reason,
|
||||
);
|
||||
_client.reject(approvalId: id, reviewer: reviewer, reason: reason);
|
||||
|
||||
/// Composite "doctor" snapshot. One round-trip per piece, run
|
||||
/// in parallel so the page populates fast.
|
||||
Future<DoctorSnapshot> doctor() async {
|
||||
final results = await Future.wait([
|
||||
_client.listCapabilities().catchError((_) => <CapabilityEntry>[]),
|
||||
_client.listApprovals(statuses: const ['pending'])
|
||||
_client
|
||||
.listApprovals(statuses: const ['pending'])
|
||||
.catchError((_) => <PendingApprovalEntry>[]),
|
||||
_client.verifyEventChain().catchError(
|
||||
(_) => VerifyEventChainResponse(),
|
||||
),
|
||||
_client.verifyEventChain().catchError((_) => VerifyEventChainResponse()),
|
||||
_client.listServices().catchError((_) => <DeclaredService>[]),
|
||||
_client.checkUpdate().catchError(
|
||||
(_) => CheckUpdateResponse(),
|
||||
),
|
||||
_client.checkUpdate().catchError((_) => CheckUpdateResponse()),
|
||||
_client.daemonPaths().catchError((_) => DaemonPathsResponse()),
|
||||
]);
|
||||
|
||||
|
|
@ -896,15 +898,11 @@ class HubService {
|
|||
pendingApprovals: approvals.length,
|
||||
eventChainTotal: chain.total.toInt(),
|
||||
eventChainVerified: chain.verified.toInt(),
|
||||
eventChainTamperedAt:
|
||||
chain.tamperedAt.isEmpty ? null : chain.tamperedAt,
|
||||
eventChainTamperedAt: chain.tamperedAt.isEmpty ? null : chain.tamperedAt,
|
||||
services: services
|
||||
.map(
|
||||
(s) => ServiceEntry(
|
||||
name: s.name,
|
||||
endpoint: s.endpoint,
|
||||
tags: s.tags,
|
||||
),
|
||||
(s) =>
|
||||
ServiceEntry(name: s.name, endpoint: s.endpoint, tags: s.tags),
|
||||
)
|
||||
.toList(),
|
||||
update: UpdateStatus(
|
||||
|
|
@ -913,8 +911,9 @@ class HubService {
|
|||
latestVersion: update.latestVersion,
|
||||
updateAvailable: update.updateAvailable,
|
||||
manifestReachable: update.manifestReachable,
|
||||
releaseNotesUrl:
|
||||
update.releaseNotesUrl.isEmpty ? null : update.releaseNotesUrl,
|
||||
releaseNotesUrl: update.releaseNotesUrl.isEmpty
|
||||
? null
|
||||
: update.releaseNotesUrl,
|
||||
reason: update.reason.isEmpty ? null : update.reason,
|
||||
),
|
||||
paths: DaemonPathsSnapshot(
|
||||
|
|
@ -958,9 +957,11 @@ class DoctorSnapshot {
|
|||
final String? eventChainTamperedAt;
|
||||
final List<ServiceEntry> services;
|
||||
final UpdateStatus update;
|
||||
|
||||
/// Filesystem paths the daemon owns. Empty fields when the
|
||||
/// hub couldn't resolve them (older daemon, no $HOME).
|
||||
final DaemonPathsSnapshot paths;
|
||||
|
||||
/// Per-source-kind capability count: bundle / system / mcp /
|
||||
/// n8n / temporal / webhook (0.12+). Pre-0.12 hubs return
|
||||
/// an empty map and the panel falls back to a flat count.
|
||||
|
|
@ -1023,14 +1024,17 @@ class CapabilityInfo {
|
|||
final String version;
|
||||
final String moduleName;
|
||||
final String kind;
|
||||
|
||||
/// Publisher segment of the fully-qualified identifier.
|
||||
/// "fai" for legacy bundles, "fai.system" for built-ins,
|
||||
/// operator-config service name for federated capabilities.
|
||||
final String provider;
|
||||
|
||||
/// Transport that delivers the capability: bundle / system /
|
||||
/// mcp / n8n / temporal / webhook. Empty string falls back
|
||||
/// to deriving from `kind` for pre-0.12 hubs.
|
||||
final String sourceKind;
|
||||
|
||||
/// Whether the version on this capability is a label rather
|
||||
/// than a hard contract. True for federation sources whose
|
||||
/// upstream service does not honour semver.
|
||||
|
|
@ -1065,6 +1069,7 @@ class SavedFlow {
|
|||
final String name;
|
||||
final String path;
|
||||
final int sizeBytes;
|
||||
|
||||
/// Verbatim `use:` strings of every step in this flow,
|
||||
/// deduplicated and sorted by the hub. Lets Studio decide
|
||||
/// whether the flow is currently runnable (every entry
|
||||
|
|
@ -1085,11 +1090,24 @@ class ModuleDetail {
|
|||
final List<String> capabilities;
|
||||
final List<String> permissions;
|
||||
final String directory;
|
||||
|
||||
/// MIME allow-list declared in the module's `module.yaml`.
|
||||
/// Empty when the module didn't declare any — caller falls
|
||||
/// back to its built-in extension heuristic.
|
||||
final List<String> acceptsMime;
|
||||
|
||||
/// Declared inputs (schema v3 verbose form when present). The
|
||||
/// flow editor uses these to render per-field input ports with
|
||||
/// canonical names + i18n tooltips.
|
||||
final List<ModuleFieldInfo> inputs;
|
||||
|
||||
/// Declared outputs (schema v3 verbose form when present).
|
||||
/// Rendered by the flow editor as distinct output ports so a
|
||||
/// step like `summarize` exposes its `response`,
|
||||
/// `model_endpoint`, `model_name`, `model_digest` separately
|
||||
/// instead of fanning them all out of one anchor.
|
||||
final List<ModuleFieldInfo> outputs;
|
||||
|
||||
const ModuleDetail({
|
||||
required this.name,
|
||||
required this.version,
|
||||
|
|
@ -1097,9 +1115,43 @@ class ModuleDetail {
|
|||
required this.permissions,
|
||||
required this.directory,
|
||||
this.acceptsMime = const [],
|
||||
this.inputs = const [],
|
||||
this.outputs = const [],
|
||||
});
|
||||
}
|
||||
|
||||
/// One declared input or output field on a module. Mirrors
|
||||
/// `proto.ModuleField`: name, type descriptor, and a locale
|
||||
/// → description map for tooltips.
|
||||
class ModuleFieldInfo {
|
||||
/// Field name, e.g. "prompt" or "model_endpoint".
|
||||
final String name;
|
||||
|
||||
/// Type descriptor (text / json / bytes / file).
|
||||
final String type;
|
||||
|
||||
/// Locale tag → description. Use [descriptionFor] to look up
|
||||
/// with English fallback.
|
||||
final Map<String, String> description;
|
||||
|
||||
const ModuleFieldInfo({
|
||||
required this.name,
|
||||
required this.type,
|
||||
this.description = const {},
|
||||
});
|
||||
|
||||
/// Returns the description in [locale] when present, else the
|
||||
/// English peer, else null. Studio passes its active locale's
|
||||
/// short code (e.g. "de") and shows the result as a tooltip.
|
||||
String? descriptionFor(String locale) {
|
||||
final exact = description[locale];
|
||||
if (exact != null && exact.isNotEmpty) return exact;
|
||||
final en = description['en'];
|
||||
if (en != null && en.isNotEmpty) return en;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class AuditEvent {
|
||||
final String eventId;
|
||||
final DateTime timestamp;
|
||||
|
|
@ -1112,6 +1164,7 @@ class AuditEvent {
|
|||
final String? flowExecution;
|
||||
final int? durationMs;
|
||||
final String? error;
|
||||
|
||||
/// Free-form JSON-string carrying event-type-specific fields.
|
||||
/// Surfaced verbatim in the audit drill-down dialog.
|
||||
final String? detail;
|
||||
|
|
@ -1163,12 +1216,15 @@ class ApprovalRecord {
|
|||
final String? payloadPreview;
|
||||
final DateTime createdAt;
|
||||
final DateTime? expiresAt;
|
||||
|
||||
/// One of: pending / approved / rejected / expired.
|
||||
final String status;
|
||||
final DateTime? decidedAt;
|
||||
|
||||
/// Reviewer handle (e.g. "alice@studio") or "system" for
|
||||
/// auto-expired approvals. Empty until decided.
|
||||
final String decidedBy;
|
||||
|
||||
/// Reject reason — empty for approve / pending / expired.
|
||||
final String reason;
|
||||
|
||||
|
|
@ -1192,9 +1248,11 @@ class SystemAiStatus {
|
|||
final String provider;
|
||||
final String endpoint;
|
||||
final String model;
|
||||
|
||||
/// off / redacted / full.
|
||||
final String privacyMode;
|
||||
final String apiKeyEnv;
|
||||
|
||||
/// Number of cached System-AI explanations currently stored.
|
||||
/// Surfaced in Doctor + Settings so operators see the cache
|
||||
/// earning its keep.
|
||||
|
|
@ -1215,17 +1273,22 @@ class AskAiResult {
|
|||
/// "" on success; otherwise one of:
|
||||
/// disabled / env_missing / network / http / parse / empty_response.
|
||||
final String errorKind;
|
||||
|
||||
/// On success: the assistant's reply.
|
||||
/// On failure: a human-readable diagnostic.
|
||||
final String text;
|
||||
|
||||
/// Round-trip latency in ms (success only). For cache hits
|
||||
/// this is the *original* generation latency.
|
||||
final int latencyMs;
|
||||
|
||||
/// True iff the answer came from the persistent cache.
|
||||
final bool cached;
|
||||
|
||||
/// ISO-8601 timestamp the cached entry was first generated.
|
||||
/// Empty for fresh answers.
|
||||
final String cachedAt;
|
||||
|
||||
/// How many times this entry has been served from cache.
|
||||
/// 0 for fresh answers.
|
||||
final int cacheHits;
|
||||
|
|
@ -1283,10 +1346,7 @@ class ChannelStatusSnapshot {
|
|||
final String active;
|
||||
final List<ChannelInfo> channels;
|
||||
|
||||
const ChannelStatusSnapshot({
|
||||
required this.active,
|
||||
required this.channels,
|
||||
});
|
||||
const ChannelStatusSnapshot({required this.active, required this.channels});
|
||||
}
|
||||
|
||||
/// One configured n8n endpoint plus the last-discovery
|
||||
|
|
@ -1320,13 +1380,16 @@ class McpClientInfo {
|
|||
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;
|
||||
|
||||
|
|
@ -1375,6 +1438,7 @@ class HardwareSnapshot {
|
|||
final int ramMb;
|
||||
final bool appleSilicon;
|
||||
final bool dedicatedGpu;
|
||||
|
||||
/// Human-readable summary the editor renders verbatim.
|
||||
final String summary;
|
||||
|
||||
|
|
@ -1414,8 +1478,10 @@ class CuratedModelInfo {
|
|||
final String id;
|
||||
final String family;
|
||||
final double paramsB;
|
||||
|
||||
/// basic / good / excellent.
|
||||
final String qualityTier;
|
||||
|
||||
/// tiny / small / balanced / large.
|
||||
final String minHwTier;
|
||||
final int contextK;
|
||||
|
|
@ -1456,20 +1522,19 @@ class CuratedSnapshot {
|
|||
final String lastReviewed;
|
||||
final List<CuratedModelInfo> models;
|
||||
|
||||
const CuratedSnapshot({
|
||||
required this.lastReviewed,
|
||||
required this.models,
|
||||
});
|
||||
const CuratedSnapshot({required this.lastReviewed, required this.models});
|
||||
}
|
||||
|
||||
/// One row from the store-index search.
|
||||
class StoreItem {
|
||||
final String name;
|
||||
final String taglineEn;
|
||||
|
||||
/// German tagline. Falls back to [taglineEn] in the UI when
|
||||
/// the index entry does not ship a localized one.
|
||||
final String taglineDe;
|
||||
final String descriptionEn;
|
||||
|
||||
/// German long-form description. Falls back to [descriptionEn]
|
||||
/// when missing.
|
||||
final String descriptionDe;
|
||||
|
|
@ -1480,30 +1545,38 @@ class StoreItem {
|
|||
final String license;
|
||||
final String repository;
|
||||
final String bestVersion;
|
||||
|
||||
/// "published", "alpha", "planned", or empty when unknown.
|
||||
final String status;
|
||||
final bool installed;
|
||||
|
||||
/// True iff the store-index marks this entry as editorially
|
||||
/// featured (curated quarterly in the bundled seed.yaml).
|
||||
/// Drives the "Featured" strip at the top of the Store page.
|
||||
final bool featured;
|
||||
|
||||
/// Optional icon URL — square PNG / SVG. Empty for entries
|
||||
/// that don't supply one; UI falls back to a category icon.
|
||||
final String iconUrl;
|
||||
|
||||
/// Ordered list of screenshot URLs; empty when none. Studio
|
||||
/// renders them as a horizontally-scrollable strip in the
|
||||
/// detail sheet.
|
||||
final List<String> screenshotUrls;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// Transport delivering this entry. Distinct from `kind` —
|
||||
/// `sourceKind` answers "HOW does the hub reach this", not
|
||||
/// "is it installable". Values: "bundle", "mcp", "n8n",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue