When to use it
One
@XpertToolProvider() class represents one independently managed MCP service. A plugin may register multiple Provider classes; each Provider has its own Tool set, enablement state, Publication, endpoint, and client configuration. Split capabilities when they have different security boundaries, lifecycles, or purposes. Middleware groups declared inside one Provider do not create additional MCP services.
How it works
The plugin loader registers the business class once. The host reads class and method metadata, validates names, schemas, behavior, and context requirements, then derives:- one native Toolset Provider;
- one Agent Middleware Strategy for each declared group in use;
- one MCP Tool definition for each MCP-enabled method;
- one Resource-backed MCP App definition for each declared App;
- one runtime-discovered virtual
TOOLSETplugin component.
.xpertai-plugin/plugin.json.
The host owns public endpoint identity. It derives a stable Publication slug from the plugin artifact namespace, Provider key, and owning scope. Do not put a client product name into the Provider key, display name, documentation, or endpoint. The legacy slug option is only a deprecated display hint and must not be used as an endpoint contract.
Prerequisites
Use compatible versions of@xpert-ai/plugin-sdk and @xpert-ai/contracts that expose the APIs used here. In particular, verify defineMcpApp, XpertToolProviderOptions.apps, and XpertMcpToolOptions.app in the installed SDK and support in the host adapter. Support for earlier hand-written native definitions does not imply support for decorator App bindings. A locally built SDK/tarball is not proof that the same API has been released to npm.
Inputs and outputs must use strict zod/v3 object schemas. Every MCP Tool requires both inputSchema and outputSchema.
Define a business Tool Provider
This Provider exposes the same read operation through Agent Middleware and MCP, with an optional interactive App for its results.OrderService is the existing typed domain service; adding the App does not change its business handler:
Exposure combinations
MCP is explicit opt-in. The host does not infer protocol schemas, risk, or visibility from TypeScript types or method names.
Add an MCP App to an existing Tool
The example still declares one Tool,orders_search. orders_search_app is an App resource, not a second callable Tool. The App supplies a UI for the existing Tool’s validated DTO; it does not need another business method, Provider, or stdio server.
The App key must be unique within the Provider, and the Tool must reference an App declared by that Provider. Use explicit
visibility: ["model", "app"] for a shared entry Tool. This is also the default for an App-bound Tool when visibility is omitted; explicitly excluding app is invalid. Use ["app"] only for genuine iframe-only Tools.
Build and serve the App
Keep frontend code separate from business handlers:entry is relative to the installed plugin root and must be included in the packed artifact. The native reader serves the declared HTML, not an arbitrary static directory. It validates plugin ownership, real-path containment (including symlinks), and a 2 MiB HTML limit. Do not use absolute paths, path traversal, or source-only entries. Run the App build as part of the plugin build, and make verify:dist reject stale or missing assets.
Xpert generates the ui:// URI and publishes the App through resources/list and resources/read with MIME type text/html;profile=mcp-app. Do not hardcode a Publication ID or implement another resource HTTP route.
The MCP Apps client loads the resource, initializes the standard bridge, and delivers the existing Tool’s structuredContent through ui/notifications/tool-result. The App can use permitted app-visible Tools for further interaction. Follow the shared bridge, i18n, and theme rules; never inject an API key or call Xpert business APIs directly from the iframe. Clients without MCP Apps rendering can still consume the Tool’s text/DTO fallback.
Tool count versus capability count
Apps do not increase the number of callable Tools. For example, 5 Tools + 2 Apps = 7 Publication capabilities, whiletools/list can correctly return 5 entries and resources/list 2. The application page’s Tool count and MCP Management’s capability count measure different things; inspect the explicit capability type, not the _app suffix. Credential scopes, visibility, policy, and required context may further reduce protocol list results. An unchanged Tool count alone is not evidence of a failed update.
Register the Provider in the plugin module
@XpertToolProvider() marks the class as injectable, but the class must still be present in the plugin Nest module’s providers:
Marketplace presentation
Runtime discovery does not require manifesttoolsets. To show the capability in plugin details, add a descriptive Marketplace contribution whose name matches componentKey and whose Provider key also matches:
name, componentKey, and provider aligned.
Context, results, and business boundaries
Xpert creates a newXpertBusinessToolContext for every invocation. It can carry the surface, tenant, organization, principal, workspace, project, conversation, Agent, execution, request, and abort signal.
- Never store the context on a singleton Provider.
- Do not accept tenant, organization, user, or credentials from model input.
- Recheck tenant, organization, and principal scope in the domain service.
- Return allowlisted DTOs instead of ORM entities or provider responses.
- MCP puts the DTO in
structuredContentand adds a short text fallback. - Give writes explicit retry semantics with
operationId, revision/CAS, and compact receipts.
getMiddlewareExtensions() hooks are for Agent lifecycle behavior such as wrapToolCall. Direct MCP execution does not automatically run the Agent wrapToolCall, so authorization, validation, persistence, idempotency, and audit must not live only in that hook.
Enable the Provider in the product
Only super administrators can enable or disable a plugin MCP Provider:- Open Plugins.
- Select Initialize on the installed plugin card.
- Find the Provider in the plugin detail dialog’s MCP section.
- Select Enable MCP server.
- Use the protected Token reveal/copy action and store the credential in the client’s secret store.
- Copy the Streamable HTTP configuration shown in the panel.
Organization scope
Provider access is always controlled for the current organization, but Publication ownership follows the plugin level:
Therefore, enabling a Provider in organization A never makes it usable in organization B. Organization B must enable it independently. For a tenant/system plugin, A and B may use the same tenant-owned endpoint after both enable access, but their grants, credentials, authenticated principals, and data scope remain isolated. The tenant Publication’s catalog, bindings, and capability policies are shared; organization-bound key scopes can further restrict a caller. An API key created for A cannot be used as B.
What enablement creates
One idempotent enable operation:- creates or reuses the Toolset in the Provider’s owning scope;
- refreshes the MCP capability catalog;
- creates or claims the host-derived stable-slug MCP Publication;
- binds all MCP Tools and Apps from the Provider;
- creates a suitable API key when no reusable key exists:
tools:list/tools:call, plusresources:list/resources:readfor Providers publishing Apps; - enables the Publication.
Generic client configuration
Xpert displays client-neutral Streamable HTTP configuration rather than coupling the plugin to a particular MCP client:XPERT_MCP_API_KEY in the client’s environment or secret store. The product’s authorized configuration-copy action can insert the current Token into the copied JSON; treat that clipboard content as a secret. Examples and previews use a placeholder. Never commit a real secret to source control, documentation, or chat, and never pass it into the App iframe.
Refresh, disablement, and policy
- Refreshing the running plugin synchronizes added, removed, and changed Tools and Apps into enabled auto-managed Publications.
- Entries with the same capability type and key retain public names, enabled states, and administrator policy overrides; new entries are bound and enabled.
- Default policy is read=
allow, write=confirm, and dangerous=deny. - A failed synchronization leaves the last valid bindings in place.
- Disabling closes an organization-owned Publication or the current organization’s access grant to a tenant-owned Publication. It does not delete history, keys, or audit.
- Explicit plugin disablement or uninstall closes associated Publications; a normal version refresh retains desired state.
Updating Tools or App bundles
- Build the plugin and App assets, run package checks and
verify:dist, then deploy/refresh the plugin at its declared scope. Editing source alone does not replace the installed runtime copy. - Restart the API when deployment returns
restartRequired. The host reconciles enabled Providers on registration and after application bootstrap; inspect synchronization errors if the last valid configuration remains. - For a manually composed Publication, open MCP Management → service → Capabilities → Refresh available capabilities, select the intended Tools and Apps, then Save. Refresh only updates the available catalog; Save changes the published bindings. This is also a manual recovery path after confirming the loaded definitions are current.
- Refresh/reconnect the client and verify Tool metadata plus the Resource operations. Keep the endpoint stable; do not recreate the service or disable other organizations to refresh it.
Verification checklist
- Provider, component, Middleware, and Tool keys are stable and conflict-free; multiple Providers from one plugin remain independently discoverable and manageable.
- Every MCP Tool has strict input/output schemas, behavior, required context, and visibility.
- Unknown fields, invalid IDs, oversized arrays, and invalid revisions are rejected.
- Execution uses the call-time principal and organization, and cross-organization credentials are rejected even when a tenant-owned endpoint is shared.
- The DTO appears in
structuredContentwithout internal paths, scope, or full entities. - Plugin build,
verify:dist, package dry-run, and lifecycle loading pass. - After enablement,
initialize,tools/list, and at least one read-onlytools/callpass. - For Apps, the Tool’s
_meta.ui.resourceUrimatches a publishedresources/listentry;resources/readreturns the expected HTML MIME, safe metadata, and current bundle using a Resource-authorized credential. - Adding Apps to existing Tools leaves the Tool name set unchanged. Test duplicate App keys, unknown references, invalid entries, scope failures, and synchronization that preserves existing policy overrides.
- Test rendering, Tool result delivery, permitted interactions, resizing, locale/theme, and text fallback in the appropriate clients. Protocol-only success does not prove visual rendering support.
- The protocol call appears in Publication audit.