@invokta/deploy
@invokta/deploy turns an engine with an MCP Streamable HTTP entry point into a
reviewable container build context, probes an existing endpoint, and inspects
OAuth discovery readiness. It is a development tool, not a runtime adapter or
deployment service.
The toolkit generates and validates text. It never runs a shell, package
manager, compiler, or container tool. init and package make no network
requests. probe makes one bounded HTTP request; inspect-oauth performs a
bounded, credential-free discovery sequence.
Install
Section titled “Install”yarn add --dev @invokta/deployThe package is native ESM, requires Node.js 22.20.0 or later, exports its command
API from the package root and its pure HTTP planner from ./scaffold, and
provides the invokta-deploy binary.
Commands
Section titled “Commands”invokta-deploy initinvokta-deploy packageinvokta-deploy probe --url <url> [--expect alive|ready] [--bearer-env NAME] [--host-header HOST] [--timeout-ms N]invokta-deploy inspect-oauth --url <url> [--timeout-ms N]invokta-deploy --helpinvokta-deploy --versionCommands are non-interactive. Progress and diagnostics go to standard error; only help and version write to standard output.
| Exit | Meaning |
|---|---|
0 |
Command succeeded, probe is healthy, or OAuth discovery is ready |
1 |
Project input, generated-file conflict, endpoint health, or OAuth inspection failed |
2 |
Usage, manifest, initialization, or unexpected command failure |
init accepts no arguments and scaffolds five user-owned files:
| File | Purpose |
|---|---|
invokta.deploy.json |
Closed deployment manifest with explicit defaults |
src/mcp-http.ts |
Production-shaped HTTP composition root |
src/http-auth.ts |
Required authentication hook that fails closed until implemented |
src/env.ts |
Bounded environment-file loader and required-name check |
.env.example |
Empty, secret-free declarations from the manifest |
Until the authentication hook is implemented, the HTTP entry point exits 1
with one sanitized instruction on standard error and no stack trace.
Existing targets are reported as skipped and are never overwritten. A valid existing manifest parameterizes the other generated files; an invalid manifest fails before writes. The five exclusive writes are sequential rather than one cross-file transaction.
The generated root expects a named engine export from src/engine.ts. It
validates environment configuration before dynamically importing the engine.
The five scaffold entries also have one pure programmatic authority:
import { createMcpHttpScaffoldFiles, starterDeployManifest,} from "@invokta/deploy/scaffold";
const files = createMcpHttpScaffoldFiles(starterDeployManifest);The returned project-relative UTF-8 text entries and their ordered array are
immutable. Planning performs no filesystem, process, network, engine, or
capability operation. The engine creator consumes this public subpath and merges
the complete plan before writing; it does not copy templates or invoke init.
package
Section titled “package”package validates, in order:
invokta.deploy.json;- package name, version, and build script;
- exactly one supported lockfile; and
- the built entry module.
Preflight failure writes nothing. A successful preflight generates:
| File | Purpose |
|---|---|
Dockerfile |
Multi-stage install, build, production prune, and non-root runtime |
.dockerignore |
Excludes Git data, dependencies, tests, coverage, and every .env* |
deploy/healthcheck.mjs |
One bounded MCP liveness/readiness request using Node built-ins |
deploy/DEPLOYMENT.md |
Operator notes derived from the manifest |
Each generated file starts with a toolkit marker. A marked file may be updated;
an unmarked existing file is a per-file conflict and remains untouched while
other files may still be generated. Status is created, updated, unchanged,
or conflict in lexicographic path order.
Writes use a private temporary file followed by rename. Identical regeneration does not rewrite bytes. Output contains no timestamp, hostname, username, absolute local path, or random value.
| Lockfile | Install text | Production prune text |
|---|---|---|
package-lock.json |
npm ci |
npm ci --omit=dev |
pnpm-lock.yaml |
pnpm install --frozen-lockfile |
pnpm install --prod --frozen-lockfile |
yarn.lock |
yarn install --frozen-lockfile |
yarn install --production --frozen-lockfile |
invokta-deploy probe \ --url https://engine.example/mcp \ --expect alive
ENGINE_PROBE_TOKEN='runtime-secret' \ invokta-deploy probe \ --url https://engine.example/mcp \ --expect ready \ --bearer-env ENGINE_PROBE_TOKEN| Option | Contract |
|---|---|
--url |
Required absolute URL whose path is /mcp or a canonical engine mount path ending in /mcp, at most 2,048 characters, without credentials, query, or fragment |
--expect |
alive by default or ready |
--bearer-env |
Environment variable name, allowed only with ready; token is non-empty visible ASCII up to 4,096 characters |
--host-header |
Visible ASCII Host override up to 255 characters; changes the header, not the network destination |
--timeout-ms |
Whole-exchange decimal timeout from 1 through 60000; default 3000 |
Plain HTTP is allowed only for literal 127.0.0.1 or [::1], not localhost.
Every option uses a separate name and value; duplicates, positionals, unknown
flags, or --flag=value are invalid.
The probe sends one MCP initialize POST, follows no redirect, performs no
retry, and reuses no connection. It accepts JSON or SSE framing and caps the
response at 1 MiB.
The current success parser requires JSON-RPC 2.0, no error, an object
result, and a non-empty result.protocolVersion. It does not require the
response ID to equal the request ID or require the negotiated version to equal
the requested version. This is probe health classification, not a complete MCP
conformance test.
| Expectation | Healthy result |
|---|---|
alive |
A 401 with a Bearer challenge or a usable 200 initialize result |
ready |
A usable 200 initialize result, with the named bearer token when supplied |
Connection failure, timeout, a malformed response, or any status outside the accepted 200/401 cases is unhealthy. A credential enters through an environment variable and never appears in argv or diagnostics.
inspect-oauth
Section titled “inspect-oauth”invokta-deploy inspect-oauth \ --url https://engine.example/mcp \ --timeout-ms 10000This is a read-only OAuth discovery readiness check. It sends an
unauthenticated MCP initialize, validates the Bearer challenge and Protected
Resource Metadata, follows the MCP-defined OAuth/OIDC metadata discovery order,
requires Authorization Code and S256 PKCE advertisements, classifies CIMD/DCR
availability, and validates a JWKS when one is advertised. A missing
jwks_uri is accepted because access tokens are not required to be JWTs.
The command sends no token, cookie, client credential, or user data. It does not register a client, perform login or consent, exchange a code, refresh a token, or mutate the remote service. It follows no redirect and performs no retry. OAuth/OIDC well-known fallbacks are separate protocol attempts.
The target path is /mcp or a canonical engine mount path ending in /mcp,
sent exactly as written, and excludes userinfo, query, and fragment. HTTPS is
required except for literal loopback addresses. --timeout-ms is one deadline
for the complete sequence, defaults to 10000, and is bounded to 1..60000.
Each response is capped at 256 KiB.
Success reports only the resource, issuer, challenged scopes, registration
classification, and JWKS status. Failure reports OAUTH_INSPECTION_FAILED
with stable stage and reason values; raw headers, response bodies, metadata,
query values, and transport messages are never printed.
Deployment manifest
Section titled “Deployment manifest”{ "schemaVersion": 1, "entry": "dist/mcp-http.js", "env": { "required": ["SUPPORT_API_TOKEN"], "optional": ["INVOKTA_HTTP_ALLOWED_ORIGINS"] }, "image": { "baseImage": "node:22-slim", "port": 3000 }, "healthcheck": { "expect": "alive" }}The schema rejects unknown keys and reports every detectable issue in JSON pointer order without echoing rejected values.
| Field | Contract |
|---|---|
schemaVersion |
Required exact number 1 |
entry |
Required relative in-project .js or .mjs path without empty or .. segments |
env.required, env.optional |
Unique names matching ^[A-Z_][A-Z0-9_]{0,127}$; at most 64 total |
image.baseImage |
Non-empty text without whitespace or NUL; default node:22-slim |
image.port |
Integer 1..65535; default 3000 |
healthcheck.expect |
alive by default or ready |
healthcheck.bearerEnv |
Environment name allowed only with ready |
The encoded document is limited to 65,536 bytes and each string to 1,024 Unicode scalars.
Generated environment contract
Section titled “Generated environment contract”The generated application code recognizes:
| Variable | Meaning |
|---|---|
INVOKTA_HTTP_HOST |
Bind host, default 127.0.0.1 |
INVOKTA_HTTP_PORT or PORT |
Bind port; the Invokta name wins |
INVOKTA_HTTP_ALLOWED_HOSTS |
Comma-separated host allowlist, required for non-loopback |
INVOKTA_HTTP_ALLOWED_ORIGINS |
Comma-separated browser origins |
INVOKTA_HTTP_MAX_BODY_BYTES |
HTTP body limit override |
INVOKTA_ENV_FILE |
Environment-file path, default .env |
Process environment, including an empty value, has precedence over the file;
file values have precedence over defaults. A missing default .env is ignored,
but an explicitly named missing file fails. The loader uses Node’s
util.parseEnv, rejects symlinks, invalid UTF-8, NUL, oversized files, invalid
names, more than 256 applied keys, or values over 4,096 Unicode scalars.
Required manifest names must be present and non-empty before the engine module
is imported. Production secrets belong in the platform; generated
.dockerignore excludes all environment files.
Errors
Section titled “Errors”Deployment errors are toolkit contracts, not EngineError values:
MANIFEST_NOT_FOUND, MANIFEST_INVALID, PACKAGE_JSON_INVALID,
LOCKFILE_MISSING, LOCKFILE_AMBIGUOUS, ENTRY_NOT_BUILT,
GENERATED_FILE_CONFLICT, WRITE_FAILED, PROBE_UNREACHABLE, and
PROBE_UNHEALTHY, plus OAUTH_INSPECTION_FAILED.
DeployError contains a stable code, exitCode, and sanitized details. It
stores no cause. renderDeployDiagnostic emits the code and message followed by
indented details.
The package root exports:
- command functions:
runDeployCli,runInit,runPackage,runProbe,runInspectOAuth; - context functions:
createDeployContext,writeDiagnostic; - command/context types:
DeployCommandName,RunDeployCliOptions,DeployCommandRun,DeployContext,DeployContextOverrides,DeployIo,DeployExitCode; - errors:
DeployError,DeployErrorCode,DeployErrorOptions, error tables, andrenderDeployDiagnostic; - manifest functions:
loadDeployManifest,parseDeployManifest, andtoDeployError; - manifest types:
HttpDeployManifest, its nested environment/image/healthcheck types, load options, result/success/failure, and issue/reason types; and - manifest constants:
deployManifestFileName,deployManifestDefaults,deployManifestLimits,deployManifestIssueMessages, andenvironmentNamePattern.
The @invokta/deploy/scaffold subpath exports
createMcpHttpScaffoldFiles, starterDeployManifest, and the immutable
McpHttpScaffoldFile entry type.
const exitCode = await runDeployCli({ argv: [ "probe", "--url", "https://engine.example/mcp", ], cwd: process.cwd(),});
process.exitCode = exitCode;The orchestration functions return an exit code and never terminate the
process. runInit, runPackage, runProbe, and runInspectOAuth accept
command arguments plus a DeployContext for embedding and tests.
Follow the deployment guide for the normal workflow and
the @invokta/mcp reference for the server being packaged.