s-m-r-t
Open the s-m-r-t source on GitHub Switch to dark color scheme
← Developer tooling

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.

src/lib/server/mcp.ts
typescript
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.

src/routes/api/mcp/+server.ts
typescript
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.
Report.ts
typescript
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.

Catalog caching is private until proven shareable

tools/list emits the required cache metadata with a one-day private default. Shared caching is intentionally exceptional: a public catalog must be a reviewed set where every allowed tool is unauthenticated, read-only, and global. The server verifies that shape, including the absence of tenant-scoped tools and principal-aware policy, and falls back to private otherwise.

public-catalog.ts
typescript
// Only for a reviewed catalog with no tenant-scoped or principal-gated tools.
toolListCache: { cacheScope: 'public', publicCatalog: true }

Authorization is terminated in front of the route

This package trusts the principal supplied by the application adapter. It does not implement an OAuth authorization server and does not validate bearer tokens itself. For a public deployment, terminate OAuth at the gateway. Validate the token signature, issuer, audience or resource, expiry, and scopes. Populate the request principal, tenant, and permissions only after validation succeeds.

  • Use one stable HTTPS issuer identifier per authorization server.
  • Issuer values are compared as exact strings; a trailing-slash difference aborts the grant.
  • Prefer a pre-registered client, then a Client ID Metadata Document, then dynamic registration.
  • Keep public tool patterns empty unless anonymous read access is deliberate.
  • Preserve tenant isolation and the allow-list for every invocation.

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.