addTool method
Adds tool, to be run by run, published as <namespace>.<name>.
Through a plugin's view the namespace is the plugin's
(PluginManifest.namespace), and namespace must be left out or be
that one. Through the root the application names one; the root refuses
a tool without it.
Throws an ArgumentError for a name that is not letters, digits, _
and -, for a published name longer than maxNameLength, and for one
already added — naming who added it. Throws a PluginException when a
plugin whose manifest does not list PluginPermission.tools adds one,
and when a plugin names another namespace than its own.
Implementation
Registration addTool(ToolSpec tool, ProjectToolRun run, {String? namespace}) {
final scope = _scope;
final String space;
if (scope != null) {
final manifest = scope.manifest;
final id = manifest.id;
if (!manifest.permissions.contains(PluginPermission.tools)) {
throw PluginException(
'plugin "$id" adds the MCP tool "${tool.name}" without asking for '
'the `tools` permission: list PluginPermission.tools in its '
'manifest, so whoever installs it sees that it talks to agents',
);
}
space = manifest.namespace;
if (namespace != null && namespace != space) {
// Refused as the plugin's other names outside its namespace are,
// naming the tool as it would have been published.
throw PluginException(
manifest.nameProblem(
'$namespace.${tool.name}',
what: 'the MCP tool',
) ??
'plugin "$id" adds its tools under its namespace, "$space", '
'not "$namespace"',
);
}
} else {
if (namespace == null) {
throw ArgumentError.value(
tool.name,
'tool',
'the application names the namespace its tools go under, its own '
'name: addTool(tool, run, namespace: \'acme_racer\')',
);
}
if (!_namespacePattern.hasMatch(namespace)) {
throw ArgumentError.value(
namespace,
'namespace',
'a namespace is lower case letters, digits, "_", "." and "-", '
'starting with a letter, as a plugin id is',
);
}
// The servers' own tools are offered beside the application's, so an
// area of theirs would shadow its tools, now or in a minor.
if (PluginManifest.reservedAreas.contains(namespace) ||
namespace == PluginManifest.reservedNamespace) {
throw ArgumentError.value(
namespace,
'namespace',
'names one of the engine\'s own areas, whose tools would shadow '
'the application\'s; name the tools after the application '
'(\'acme_racer\')',
);
}
space = namespace;
}
if (!_namePattern.hasMatch(tool.name)) {
throw ArgumentError.value(
tool.name,
'tool',
'a tool\'s name is letters, digits, "_" and "-"; the namespace '
'and its dot are added for it',
);
}
final name = published(space, tool.name);
if (name.length > maxNameLength) {
throw ArgumentError.value(
name,
'tool',
'is ${name.length} characters published; at most $maxNameLength',
);
}
final owner = scope == null
? 'the application'
: 'plugin "${scope.manifest.id}"';
if (named(name) case final ProjectTool taken) {
throw ArgumentError.value(
name,
'tool',
'is already added, by ${taken.owner}; $owner cannot add it again',
);
}
final added = ProjectTool._(
tool.copyWith(
name: name,
meta: <String, Object?>{
'flutter3d/schemaVersion': ?_store.schemaVersions[space],
},
),
run,
namespace: space,
owner: owner,
scope: scope,
sequence: _store.sequence++,
);
_store.tools.add(added);
_store.changed();
// By sequence rather than identity: a version declared later replaces
// the entry with one carrying it (see [declareSchemaVersion]).
final registration = Registration(() {
final before = _store.tools.length;
_store.tools.removeWhere(
(ProjectTool t) => t._sequence == added._sequence,
);
if (_store.tools.length != before) _store.changed();
});
scope?.track(registration);
return registration;
}