Skip to main content

@happyvertical/smrt-app-cli

Reusable, application-branded CLI support for s-m-r-t apps. It combines resource discovery, generated command invocation, device-code authentication, local configuration, and an MCP stdio bridge without coupling the CLI to one app.

Use this package when a deployed s-m-r-t application should expose a branded <app> <resource> <command> CLI. Use @happyvertical/smrt-cli for framework development, schema, and code-generation commands.

Installation​

pnpm add @happyvertical/smrt-app-cli

Node.js 26 or newer is required.

The published package also exposes a configuration-driven smrt-app binary. It lets policy runners and app operators use the canonical CLI outside a consumer repository without maintaining a branded wrapper:

SMRT_APP_NAME=work \
SMRT_APP_ENV_PREFIX=WORK \
SMRT_APP_CONFIG_DIR=happyvertical-work \
SMRT_APP_DEFAULT_SERVER_URL=https://work.example \
pnpm dlx --package @happyvertical/smrt-app-cli@X.Y.Z \
smrt-app -- auth status

Pin the exact published package version in automation. The four application settings are required and are non-secret; command arguments after -- are forwarded unchanged to createAppCli. The same values may be passed as --name, --env-prefix, --config-dir, and --default-server-url before the delimiter. Invalid or incomplete configuration fails before the CLI performs network or credential access. The executable also applies the HTTPS/loopback policy to the effective server URL selected from app-specific environment or persisted configuration, and to command-level server overrides.

To run the same CLI's stdio MCP bridge, configure its explicit identity and select bridge mode:

SMRT_APP_NAME=work \
SMRT_APP_ENV_PREFIX=WORK \
SMRT_APP_CONFIG_DIR=happyvertical-work \
SMRT_APP_DEFAULT_SERVER_URL=https://work.example \
SMRT_APP_MCP_SERVER_NAME=work-mcp \
SMRT_APP_MCP_SERVER_VERSION=1.0.0 \
smrt-app --stdio-mcp

SMRT_APP_* is reserved for non-secret executable configuration. Runtime credentials continue to use the app-specific environment/config resolution implemented by createAppCli; never pass bearer tokens as executable options.

Quick start​

#!/usr/bin/env node
import { createAppCli } from '@happyvertical/smrt-app-cli';

const cli = createAppCli({
name: 'acme',
defaultServerUrl: 'https://app.acme.test',
});

await cli.run(process.argv.slice(2));

The resulting CLI includes:

acme auth login [--server <url>] [--no-open]
acme auth status
acme auth logout
acme resources [--json] [--debug]
acme mcp
acme <resource> <command> [flags]

Resources and their commands are discovered from the app's /_resources surface. JSON Schema becomes CLI flags, while unsupported schema shapes are reported instead of silently guessed.

Discovery conformance artifact​

createResourceListHandler() includes an artifact beside the compatible user, warnings, and resources response fields. The artifact has a stable schema selector, version, canonical ordering, and a sha256:<hex> digest. The CLI validates it before using its embedded discovery payload.

For a released downstream app, install the exact published @happyvertical/smrt-users version and pin a captured artifact like this:

import {
validateDiscoveryConformanceArtifact,
} from '@happyvertical/smrt-users/app-contract';

const response = await fetch('https://app.example/api/_resources');
const body = await response.json();
const artifact = validateDiscoveryConformanceArtifact(body.artifact);

console.log(artifact.schema, artifact.version, artifact.integrity.digest);
// Store all three selectors and the canonical JSON in your packaged-runtime test.

The result contract embedded in the artifact names the common code, message, details, retryable, correlationId, idempotencyKey, and expectedVersion metadata. mcp tools and mcp call print this as a JSON success/error envelope. HTTP failures may use either a conventional top-level metadata object or { error: metadata }; both normalize to that same shape. Generated action failures use the latter form with { error: { ok: false, code, message, status, ... } }; the CLI reports the HTTP status beside that normalized metadata. The stdio bridge carries it in MCP _meta under io.happyvertical/smrt. It deliberately does not synthesize MCP structuredContent.

Add an app-specific command​

const cli = createAppCli({
name: 'acme',
extraCommands: [
{
name: 'doctor',
description: 'Check the deployed app',
async run(_args, context) {
const health = await context.requestJson('/api/health');
context.stdout.write(`${JSON.stringify(health, null, 2)}\n`);
},
},
],
});

App-specific commands win over built-ins. Avoid names that may collide with a discovered resource slug.

MCP bridge​

The same app CLI can expose the deployed application's MCP surface to local stdio clients:

await cli.startMcpBridge({ name: 'acme-mcp', version: '1.0.0' });

The bridge forwards to /api/mcp by default. When the app mounts its MCP route elsewhere, set mcpPath on createAppCli() or pass it per call (startMcpBridge({ mcpPath: '/mcp' })). The binaries take --mcp-path=/mcp (smrt-mcp-bridge, or SMRT_MCP_PATH) and --mcp-path / SMRT_APP_MCP_PATH (smrt-app). The path must be same-server and absolute.

For a local-profile s-m-r-t app, mint a credential with smrt app token --scopes <scope> in the app checkout. The token is printed once. Then give the bridge the loopback server and that token through the app's environment prefix. This Claude Desktop entry is an example:

{
"mcpServers": {
"notes": {
"command": "npx",
"args": ["smrt-mcp-bridge", "--env-prefix=NOTES", "--mcp-path=/mcp"],
"env": {
"NOTES_SERVER_URL": "http://127.0.0.1:5173",
"NOTES_TOKEN": "smrt_mcp_…"
}
}
}
}

The token is sent only when <PREFIX>_SERVER_URL matches the target server. Revoke a token with smrt app token revoke <id>.

The package also ships the generic smrt-mcp-bridge binary. The remote app surface is typically mounted with @happyvertical/smrt-app-mcp. The binary rejects non-loopback plaintext HTTP origins before resolving or sending stored credentials; use HTTPS for remote application servers.

The default native MCP bridge preserves upstream tool catalog order and forwards tools/list, tools/call, resources/list, and resources/read through SDK v2. Tool catalogs and resource responses use a zero TTL with private cache scope so subsequent requests recheck upstream authorization. The bridge uses the configured local app credentials and never opts into shared caching.

Only the explicit transport: 'legacy-rest' compatibility mode sorts its tools/list catalog by tool name and emits a one-day private cache lifetime. That mode uses the REST-shaped tools endpoints and does not forward resources.

Configuration and authentication​

  • name supplies the display name and default environment-variable prefix.
  • envPrefix overrides that prefix; configDir overrides the local config namespace.
  • defaultServerUrl is used only when neither environment nor stored config supplies a server.
  • Device-code login distinguishes pending approval, expiry, transient network failure, and hard rejection.
  • Tokens and server configuration stay in the app-specific local config path.
  • Device-flow responses identify their issuer. Tokens are keyed by that exact issuer and are not reused when the configured server changes.

For standards-based remote MCP OAuth registration, use registerMcpClient() (Client ID Metadata Document first, executable RFC 7591 DCR fallback with application_type) and follow the remote MCP authorization guide.

Public API​

ExportPurpose
createAppCli()Build the branded CLI and MCP bridge
createMcpStdioBridge() / runMcpStdioBridge()Lower-level bridge control
fetchResourceList()Fetch the deployed resource catalog
invokeCommand()Invoke one generated resource command
buildFlagParser()Convert supported JSON Schema to flags
loadCliConfig() / saveCliConfig()Read and write namespaced CLI config
resolveMcpClientRegistration()Inspect the selected Client ID Metadata or DCR path
registerMcpClient()Use Client ID Metadata or execute the DCR fallback

Development​

pnpm --filter @happyvertical/smrt-app-cli test
pnpm --filter @happyvertical/smrt-app-cli typecheck
pnpm --filter @happyvertical/smrt-app-cli build

Maintainer patterns and authentication invariants are documented in AGENTS.md.

For remote deployments with a known OAuth issuer, set CliConfigContext.expectedCredentialIssuer to its exact identifier. Stored credentials must match both server and issuer; environment credentials also require ${PREFIX}_TOKEN_ISSUER. This does not replace the terminal device flow with browser OAuth. See the remote authorization contract.

Generate local desktop plugin metadata​

Use generateDesktopPluginArtifacts in the product's packaging step. Pass public product metadata from the portable plugin manifest, such as the checked-in Kitchen manifest. The required fields are name (lowercase letters, digits and single hyphens), version (SemVer), description, and author.name. The optional extensions["com.openai"].interface.displayName defaults to name.

import { generateDesktopPluginArtifacts } from '@happyvertical/smrt-app-cli';

const files = generateDesktopPluginArtifacts(productManifest, {
platform: 'darwin',
executable: 'server/smrt-mcp-launcher',
});

The result maps distribution-root-relative paths to deterministic UTF-8 JSON. Materialize the files in a fresh staging directory. Place the executable under the same server/ path in each plugin directory. Run the generator for the target platform (darwin, win32, or linux), using an .exe launcher filename for Windows. The launcher must resolve the installed app's bundled runtime and obtain local authorization at launch. The generator reads public metadata only and takes no environment, MCP arguments, endpoint, or authentication configuration.

  • claude-desktop/<name>/manifest.json is the binary extension manifest in the staging directory for mcpb pack. Include the executable when packing. The manifest declares the target platform and resolves the launcher from the installed extension directory. Claude Desktop Extensions · MCPB manifest format
  • plugins/<name>/plugin.json and mcp.json are the portable ChatGPT desktop plugin, listed in .agents/plugins/marketplace.json. OpenAI plugin packaging · Desktop-only plugins · Agent Plugins stdio path rules
  • plugins/<name>/.claude-plugin/plugin.json and .claude-plugin/marketplace.json provide Claude Code marketplace compatibility. Claude Desktop installs the MCPB extension from the separate staging directory. Claude Code marketplaces

Each generated catalog contains one entry. For a shared catalog, merge the entry into the catalog maintained by the packager. Live installation and execution in Claude Desktop and ChatGPT desktop require the production launcher and separate host verification.