Skip to main content
@widgentic/mcp/sdk exports one function, createWidgenticServer(options?), returning a connectable McpServer from the official MCP TypeScript SDK 2.x (@modelcontextprotocol/server) with the full wiring: the nine tools, the MCP Apps declaration, the app template resource, output slimming and image inlining. The SDK packages are optional peers, so install them alongside:
The smallest server serves exactly the built-in kinds and themes over stdio, to clients on protocol revision 2026-07-28 and on the 2025-era revisions alike:

The stdio example

The repository’s examples/mcp-server/main.ts is the template for a deployment with your own widgets compiled in. Run it from a checkout with npm run mcp, or register it with a host:
Author widgets in the designer, export them as TypeScript (the export matches the example’s widgets/ module shape), register them into a catalog and hand it to the assembly. Compiled-in widgets bind their actions inline, so the action source walks the same definitions:
A stdio server runs on the operator’s own machine, which is why the example grants execute.

Options

With no options the assembly serves the built-ins; everything else is your explicit choice. Catalog and themes are passed in because the trust decision belongs where the API key is read.

Transports and environment

The assembly is transport-agnostic. Serve it through the SDK’s entries, which speak protocol revision 2026-07-28 and the 2025-era revisions from the same factory: serveStdio(factory) over stdio, and createMcpHandler(factory) over Streamable HTTP. A server connected to a transport directly (in-memory pipes in tests, for example) speaks the 2025 era only. Over HTTP the handler calls the factory once per request with the inbound Request. Resolve the caller’s principal from the presented API key there, and build that request’s server. Composition caches nothing, so one principal’s widgets never reach another’s session. A key that resolves to no principal degrades to the anonymous catalog, never to an error, and is never logged. On Node, wrap the handler with toNodeHandler from @modelcontextprotocol/node:
The handler verifies no token and validates no Host or Origin header. Put your own checks, CORS and body limit in front of it, as the self-hosting example does. Browser hosts on 2026-07-28 send Mcp-Method and Mcp-Name headers, so allow them in CORS. Two environment variables are read each time a server is constructed:
  • WIDGENTIC_ASSUME_UI — 1 or true slims the default-format render_widget result (a one-line confirmation instead of the full HTML text block) for calls that reveal no client capabilities. The client’s own capabilities always decide first, in either direction: those a 2025-era session negotiated at initialize, or those a 2026-07-28 request carries in its _meta. The fallback matters for stateless 2025-era HTTP, where each request builds a server that never saw initialize. Explicit format values are never slimmed.
  • WIDGENTIC_INLINE_IMAGES — 0 or false disables server-side image inlining. By default the iframe-facing surfaces of a result (structuredContent.html, structuredContent.tree and the ui://widgentic/page/<kind> resource) get https image sources rewritten to data: URIs through the guarded fetch, because Apps-host sandboxes block external images.

Give it a store

@widgentic/mcp/store turns an API key into a principal. createMemoryStore(seed?, limits?, options?) serves tests and demos; createFileStore(dir, options?) reads this layout:
settings(principalId) and, on writable stores, putSettings(principalId, update) hold one settings record per principal: a write replaces the sections it names (null removes one) and keeps the rest. Its defaultThemes section names the stored theme light and dark hosts get when a render names no theme; composeThemes turns it into the registry’s defaults, and a theme named there cannot be removed (THEME_IN_USE). Its resourceDomains section lists up to 20 domains the caller’s frames may load images from directly, stored as canonical origins; an entry the rule refuses is INVALID_DOMAIN, and composeResourceDomains yields the list for createWidgenticServer. Read the record once per request with readPrincipalSettings and pass it to both as { settings }. Options carry limits (how many widgets, themes, schemas, actions and secrets a principal may hold, the bytes per entry and the template nodes per widget — the defaults are on Limits), an onDiagnostic sink and a cipher for secrets. Per request:
composeCatalog and composeThemes return fresh instances every time, each with a diagnostics array: an invalid, oversized or built-in-shadowing entry is skipped with a diagnostic, never fatal. Keys are stored as sha256: digests and compared in constant time (generateKey, hashKey, verifyKey); a key carries read and optionally execute, fixed at creation.

SQLite

@widgentic/mcp/store/sqlite exports createSqliteStore(path, options?), a WritableWidgetStore over a single database file using the Node runtime’s built-in SQLite — no dependency, not even an optional peer. It opens the file in WAL mode so a read-only MCP process and a writing authoring app can share it, runs every write in one transaction (a refusal leaves nothing behind), and stores key digests and secret ciphertext only. Options carry limits, onDiagnostic and a cipher, like the other implementations, and the returned store adds close(). It is a single-node design: reads and writes are synchronous statements, bounded by the store limits — past that ceiling you want a server database, not this adapter. The self-hosting example runs on it.

Cosmos DB

@widgentic/mcp/store/cosmos exports createCosmosStore(options), a WritableWidgetStore over two containers: data, partitioned by /principalId with one document per entry (profile, widget:<kind>, theme:<name>, schema:<name>, action:<name>, secret:<name>), so a principal’s catalog is one single-partition query; and keys, partitioned by /digest, so key resolution is a point read. It takes an endpoint and an Azure credential (managed identity in deployment) plus optional databaseId, dataContainerId, keysContainerId, limits, cipher and log — deliberately no account-key or connection-string option. Under Cosmos RBAC the serving identity can hold the read-only role, so a write from the MCP server fails at the service. @azure/cosmos and @azure/identity are optional peers of this entry.

Mount the authoring surface

The write side — how people author the entries the store serves — ships as @widgentic/mcp/authoring. handleAuthoringRequest({ method, path, body, context }, deps) is a pure core over the writable store: widgets, themes, schemas and shared actions (the path names the entry; the body can never redirect it), the principal’s settings at /settings, the action test call through the guarded fetch, write-only secrets, and API keys minted once with fixed scopes. createAuthoringHttpHandler(deps, resolveContext) mounts it on node:http under /api. Authentication is yours: the surface begins where it ends. Your resolveContext(req) returns { principalId, subject?, label? } however you authenticate — a session cookie, a trusted proxy header, or a fixed principal for a single-user deployment — and identity routes (linked accounts) exist only when you supply a subject. Two rules are the surface’s own and cannot be dropped by a host: a presented widgentic API key is refused with 401 KEY_NOT_A_SESSION before any store access (keys travel into prompt-injectable hosts and must never author), and nothing in a request can name a principal other than the one you resolved.

Secrets

Http actions reference secrets by name; @widgentic/mcp/secrets stores them as envelope-encrypted records. encryptSecret(value, cipher) generates a fresh 256-bit data key, encrypts the value with AES-256-GCM, wraps the data key through the cipher port and returns { alg, kekVersion, wrappedKey, iv, ciphertext, tag }; decryptSecret reverses it and rewrapSecret moves a record to a newer key version without decrypting the value. Values are 8 to 4096 bytes. A SecretCipher only wraps and unwraps data keys. createLocalCipher(hexKey) keeps a 64-hex-character key in memory (generateLocalKek() makes one) — the cipher for development rigs and for self-hosted single-node deployments, where the operator supplies the key from a mounted file or an environment variable and protects it like the credentials it encrypts: whoever can read that material can decrypt that deployment’s records. Vault-backed custody stays the stronger arrangement and is what widgentic’s own deployment uses. @widgentic/mcp/secrets/keyvault exports createKeyVaultCipher({ keyId, credential, previous? }), which wraps and unwraps through the vault’s cryptographic operations so the key-encryption key never enters the process; the identity needs only the wrap/unwrap role on that key, and previous maps older key versions to clients until records are re-wrapped. Hand the cipher to the store’s cipher option; without one the store refuses putSecret and secretValue with NO_CIPHER. Secrets are injected only at execution, never displayed, and redacted from every message the server emits.