Developer tooling
Runtime MCP surfaces for application agents
Generated local MCP, hosted application MCP, and WebMCP use the same @smrt() model. They run at different boundaries and do not give an application agent the same identity or authority.
Verified against s-m-r-t 0.42.4
Describe the server once
createMcpAppServer wraps the generated core tools with an application allow-list and returns listTools and callTool. The allow-list determines which classes are reachable. Public tool patterns determine what an unauthenticated caller can see. The tool policy evaluates each principal. Workflow assertions run before generated dispatch.
import { createMcpAppServer, McpAccessError } from '@happyvertical/smrt-app-mcp';
import { adminResources } from '$lib/admin/resources';
import { getDbConfig } from './db';
export const mcpServer = createMcpAppServer({
smrtOptions: () => ({ db: getDbConfig() }),
serverInfo: { name: 'my-app', version: '0.1.0' },
allowedClassNames: adminResources.map((r) => r.className),
// Empty by default, so nothing is anonymous until an operator opts in.
// Until a pattern is listed, no tool passes the base rule for a caller with
// no principal, and the unauthenticated branch below is never reached.
publicToolPatterns: () =>
(process.env.MY_APP_PUBLIC_MCP_TOOLS ?? '')
.split(',')
.map((s) => s.trim())
.filter(Boolean),
toolPolicy: ({ tool, principal }) => {
if (!principal) return tool.name === 'application_get';
if (principal.kind === 'human') return principal.roles?.includes('admin') ?? false;
return principal.kind === 'service' && principal.scopes?.includes('mcp:applications') === true;
},
workflowAssertions: {
application_update: (args, user) => {
if (!user?.id) throw new McpAccessError(401, 'sign in first');
args.approvedByUserId = user.id;
}
}
});Mount it as one route
mountMcpRoute is the modern, fetch-style Streamable HTTP endpoint. It serves server/discover, tools/list, and tools/call. The tools capability is always present; the optional tasks extension is advertised only when an allowed object enables a task action. Tool discovery is deterministically ordered by name.
import { mountMcpRoute } from '@happyvertical/smrt-app-mcp/sveltekit';
import { mcpServer } from '$lib/server/mcp';
export const POST = mountMcpRoute(mcpServer);Opt long-running actions into durable tasks
Long-running item actions can opt into the experimental io.modelcontextprotocol/tasks extension. Tasks are disabled by default. List each task action in the object’s MCP configuration. Align the jobs runner dispatch allowlist when the class uses backgroundEligible markers.
- Task-aware clients use tasks/get, tasks/update, and tasks/cancel to observe or control the durable job.
- An application deployment must run a TaskRunner for the mcp-tasks queue; generated stdio servers start that worker automatically.
- Task lifecycle calls require a stable authenticated principal id, plus tenantId for tenant-scoped objects.
- backgroundEligible is restrictive once the first method is marked, so mark every method the TaskRunner may dispatch.
import { backgroundEligible } from '@happyvertical/smrt-jobs';
@smrt({
mcp: { include: ['generateReport'], tasks: ['generateReport'] }
})
class Report extends SmrtObject {
@backgroundEligible()
async generateReport() {
return buildReport(this.id);
}
}Stateless by construction
The route builds a fresh protocol server for every HTTP request. It does not issue or depend on a session id, sticky load-balancer routing, or a held event stream, so it runs behind ordinary round-robin deployment. The mount exposes no subscription capability, and subscription requests are refused as a JSON-RPC error before any stream opens.
- Persist multi-step workflow progress in application objects.
- Pass the explicit object id back into the next tool call.
- Neither MCP sessions nor request principal state are held between nodes.
Request metadata is validated, not trusted
Stock MCP clients send a required Mcp-Method header, and Mcp-Name for a tool call. The mount validates them against the JSON-RPC body and returns the protocol HeaderMismatch error, code -32020 with HTTP 400, for a missing or mismatched header. Header presence is not authentication.
Policy runs on discovery and on the call
The tool policy evaluates every tool that passes the allow-list and the base public or authenticated rule. The policy runs during discovery and direct calls. Returning false hides the tool from discovery and denies a direct call with the non-retryable mcp_tool_access_denied code. A thrown policy error is treated as a denial, so policy failures fail closed.
- On the modern mount the denial arrives as a JSON-RPC protocol error carrying the code and a retryable flag in its data.
- The deprecated REST aliases instead return the older ok, code, message, status, and retryable body.
- Neither shape includes tool, principal, scope, or policy-error detail.
- A tool outside the application allow-list receives the safe not-found behavior.
- Unauthenticated callers see only the tools selected by public tool patterns.
Identity comes from the application
SvelteKit mounts resolve the request principal once and use it for both discovery and calls. McpAppPrincipal has optional id, kind, roles, and scopes fields. This flexible shape represents a person or scoped service without encoding the application identity model. A missing principal means the request is unauthenticated.
- resolvePrincipal is the current hook for applications that store the principal elsewhere.
- resolveUser remains as a legacy compatibility alias.
- resolveAuthenticated is a deprecated legacy boolean gate, consulted only when resolvePrincipal is absent. Only a false result clears the principal. An older mount that returns true keeps its calls without a user. Discovery then uses the old boolean behavior. Migrate the mount to resolvePrincipal.
Generated stdio stays local
The generated local MCP server runs beside the application. It obtains credentials from its environment and has no per-request authorization principal, so it must not be exposed remotely. Use the smrt-mcp-bridge binary to reach a deployed application from a local stdio client. @happyvertical/smrt-app-cli publishes this bridge. It authenticates through the first-party terminal device flow. The bridge sends stored tokens only to their associated server.
WebMCP stays in the browser session
WebMCP is not the local stdio server and it is not the hosted application MCP route. The page registers selected tool descriptions in a compatible browser. Tool execution uses the generated REST client as the signed-in page user. Existing authentication, tenant, permission, writable-field, and field-policy checks stay in the request path.
- The browser page selects which generated tool descriptions it registers.
- The page session supplies the application identity. WebMCP does not introduce a coding-agent identity.
- The browser surface does not give the Development MCP server access to application data.
- Use hosted application MCP when a remote agent runs outside the browser session.