Skip to content

fix(sdk): CLI tasks honour every redis form, and a build opens none of a server's stores - #1040

Merged
alexmercerpo merged 4 commits into
fix/h01-browser-node-builtinsfrom
fix/h02-cli-tasks-redis-url
Nov 3, 2026
Merged

alexmercerpo merged 4 commits into
fix/h01-browser-node-builtinsfrom
fix/h02-cli-tasks-redis-url

Conversation

@frontegg-david

@frontegg-david frontegg-david commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #1039 (native stack #1029).

What and why

Fixes #1035.

In CLI mode, a top-level redis: { url: 'redis://…' } didn't reach the task store. The generated CLI then kept its tasks in memory, or in the Redis of REDIS_URL / REDIS_HOST, while the same config as a server kept them in the Redis it names. A detached worker of tasks.runner: 'cli' could not see a task its host created.

Why server mode worked

Both modes create the task store through the same code: the scope hands the top-level redis to createTaskStore(). The difference is the parser:

  • The server parser (frontMcpMetadataSchema) validates redis with redisOptionsSchema. Its URL branch reads { url } into { provider: 'redis', host, port, password, db, tls, keyPrefix, defaultTtlMs }.
  • The CLI parser (parseFrontMcpConfigLite) passed redis through as z.any(), so the scope got the raw { url }.
  • createTaskStore() mapped redis into a storage config only when it had a host. Any other shape fell through, without an error, to createStorage's auto-detection: REDIS_URL / REDIS_HOST, else memory.

A server declared with @FrontMcp was not affected: the decorator runs the full parser, so the CLI got the normalized config. A config exported as an object was: an entry that exports one, or connect(config, { mode: 'cli' }).

The change

  • The lite parser takes redis from frontMcpBaseSchema.shape, as it already does for tasks, jobs and sqlite. CLI mode now gets the same normalized config as the server, and an unusable redis fails at startup with the server's error. That covers every CLI-mode consumer of the top-level redis: the task store, the job stores, and plugins that read it through getGlobalStoreConfig() (CachePlugin / RememberPlugin with type: 'global-store' read host and fell back to localhost on a URL).
  • createTaskStore() and createElicitationStore() normalize their redis option with redisOptionsSchema, the schema the server parser uses, instead of their own host check. Both factories are public, and each had the same host-only mapping. They now take every form (url, host / port, with or without provider: 'redis'), with password, db and TLS (rediss://, or tls: true). An unusable one throws TaskStoreNotSupportedError / ElicitationNotSupportedError with the schema's message, instead of falling back. The shared helper, normalizeRedisOptions(), is internal (not exported from the package).
  • createForManifest() leaves out every store before it validates the config: the top-level redis, pubsub and sqlite, jobs.store, tasks.redis and tasks.sqlite. frontmcp build boots the server in this mode only to read it, and reads none of them. It left out redis, pubsub, sqlite and jobs.store only after validating them, so with the lite parser now validating redis, a config object with redis: { url: process.env['REDIS_URL'] } would stop building wherever REDIS_URL isn't set, as one with sqlite.encryption.secret, jobs.store.redis.url or tasks.redis.url from an unset variable already did. It kept tasks.redis and tasks.sqlite, so the build also connected to that Redis and created that SQLite file.
    • One rule for every store, and this one rather than validating them all: the build never needs them, and validating them would stop frontmcp build --target cli and --target mcpb (whose user_config reaches the server only as environment variables when it runs) for any config whose URLs or secrets exist only at runtime. The deploy targets go on without a manifest when the boot fails.
    • A @FrontMcp class still differs: the decorator validates its whole config with the server's schema when the entry module loads, in every mode, FRONTMCP_SCHEMA_EXTRACT included, so an unset variable there stops the build's boot before createForManifest() runs. Validating at build time would not remove that difference, only copy it onto config objects; deferring the decorator's store validation during schema extraction is a separate change.

The other stores

Store CLI mode Result
Tasks Opened from the top-level redis (or tasks.redis) The bug. Fixed by both changes above.
Jobs Opened from the top-level redis Already honoured a url: createJobStateStore() hands url to createRedisClient(). It now gets the normalized fields instead, which createRedisClient() takes the same way. Keys stay under mcp:jobs: unless keyPrefix is set.
Elicitation, persistent questions Not created in CLI mode No CLI-mode gap. createElicitationStore() had the same host-only mapping for its own callers, fixed here. Persistent questions already normalize with redisOptionsSchema (resolveQuestionStorage).
Sessions, transport persistence, event store, HA Not created in CLI mode No gap: in server mode they get the parsed config.

User-facing change

  • What changed:
    • CLI mode (the generated CLI from frontmcp build --target cli, connect(config, { mode: 'cli' }), FrontMcpInstance.createForCli()) validates and normalizes the top-level redis with the server's schema.
    • createTaskStore({ redis }) and createElicitationStore({ redis }) accept every form of the redis option, and throw on an unusable one.
  • Before → after (CLI mode, for a server config given as an object):
    • redis: { url: 'redis://:pw@cache:6380/2' }: tasks were kept in memory, or in the Redis of REDIS_URL / REDIS_HOST → tasks are kept in cache:6380, database 2, with the password. rediss:// turns TLS on. This moves CLI tasks to the configured Redis.
    • An unusable redis, such as { url: 'http://cache:6379' }, a url that contradicts a field beside it, or { url: undefined } from an unset variable: the CLI ran with its tasks in memory → it fails at startup with the server's error (redis.url must use the redis:// or rediss:// scheme, got "http://", redis port contradicts redis.url…, or Zod's Invalid input on redis), and exits with code 1.
    • CachePlugin / RememberPlugin with type: 'global-store' and a URL-form redis: connected to localhost → connect to the URL's host, port, password and database.
    • frontmcp build of a config object whose redis, pubsub, sqlite, jobs.store, tasks.redis or tasks.sqlite takes a URL or secret from an unset variable: sqlite, jobs.store (since CLI mode doesn't validate the jobs config #1032) and tasks.* stopped the boot (the CLI and mcpb targets failed; the others wrote no manifest) → builds. A @FrontMcp class still needs the variables where it builds.
    • frontmcp build of a server with tasks.redis or tasks.sqlite: connected to that Redis, or created that SQLite file → opens neither.
  • Before → after (createTaskStore() / createElicitationStore(), any mode):
    • redis: { url }: ignored, so the store used REDIS_URL / REDIS_HOST or memory → the store uses that Redis.
    • A redis without host or url (such as { port: 6379 }), or an invalid URL: ignored the same way → TaskStoreNotSupportedError / ElicitationNotSupportedError, Invalid Redis configuration: <reason>.
  • frontmcp.dev pages affected (if known): the Redis setup page (the url form), the CLI build / deployment page (tasks in the generated CLI), and the background tasks page if it lists the redis forms.
  • Skills catalog entries updated:
    • frontmcp-setup/references/setup-redis.md: the generated CLI and the two factories read every form and refuse an unusable one.
    • frontmcp-deployment/references/build-for-cli.md: the CLI reads redis as the server does, its tasks go to that Redis, and frontmcp build of a config object doesn't need the variables of its stores.
    • frontmcp-deployment/references/build-manifest.md: the boot leaves out tasks.redis and tasks.sqlite too, before validating, and a @FrontMcp class still needs the variables.

Tests

Unit (libs/sdk):

  • task/store/__tests__/task-store.factory.spec.ts: five redis forms (url with password and database, rediss://, provider + url with a password beside it, provider + host, host only) reach createStorage as the same connection fields; three unusable ones (http://, a url and a contradicting port, no host) throw TaskStoreNotSupportedError and open no storage.
  • elicitation/store/__tests__/elicitation-store.factory.spec.ts: a rediss:// URL and a host reach createStorage; an http:// URL throws ElicitationNotSupportedError.
  • common/metadata/__tests__/parse-lite.spec.ts: the lite parser reads url, rediss:// and host forms into the server's normalized shape, and refuses an http:// URL with the server's message.
  • scope/__tests__/scope-cli-redis.spec.ts (new): createForCli() with redis: { url: 'rediss://:secret@cache.internal:6380/3' } keeps the tasks in that Redis (taskBackendType: 'redis', with host, port, password, database and TLS), and an http:// URL fails startup before any store opens.
  • manifest/__tests__/build-manifest.boot.spec.ts: buildManifest() reads a config whose redis, pubsub, sqlite, jobs.store, tasks.redis or tasks.sqlite takes a URL or secret from an unset variable, and a server with tasks.redis or tasks.sqlite connects to no Redis and writes no file.

Each new test failed before the fix: the URL forms got memory and the unusable ones started. The manifest tests failed for sqlite, jobs.store and tasks.* (and for redis with the lite parser change alone), and the build made 4 connections to tasks.redis and created the tasks.sqlite file.

E2E (apps/e2e/demo-e2e-cli-exec/e2e/cli-build-tasks-redis.e2e.spec.ts, new): builds a CLI from a scratch project whose config object has redis: { url: process.env['TASKS_REDIS_URL'] }, with the variable unset at build time.

  • An http:// URL stops the CLI at startup with the server's message (it ran the tool before).
  • Against a testcontainers Redis with requirepass set, running a tool with redis://:<password>@host:port/5 shows the task store's PING in database 5 on MONITOR (nothing reached Redis before). It needs Docker, like demo-e2e-distributed, and is skipped with SKIP_REDIS_TESTS=1.

Run locally:

  • nx test sdk: 666 suites, 9,335 tests passed.
  • nx test demo-e2e-cli-exec: 38 suites, 213 tests passed, the Redis test included (Docker). The new spec also passes with SKIP_REDIS_TESTS=1 (the Redis test is skipped).
  • nx test demo-e2e-bundle-size: 9 suites, 42 tests passed. Browser bundles: create() + one tool 1,294,909 bytes (was 1,294,963), with WebMcpPlugin 1,302,093 (was 1,302,147), Vite initial chunk 761,771 (unchanged). No budget changed.
  • nx test skills: 249 passed. nx lint sdk skills: no errors.
  • The changed specs and the e2e spec compile with tsc --noEmit.

… the server does

The lite config parser of CLI mode passed the top-level redis through as z.any(), so a redis: { url } reached
createTaskStore raw. The factory mapped redis into a storage config only when it had a host, and fell back to
REDIS_URL / REDIS_HOST or memory otherwise, so the generated CLI kept its tasks where its detached workers could not
find them. The lite parser now validates redis with the server's schema, and createTaskStore and
createElicitationStore normalize their redis option with that schema, so every form reaches the store with its
password, database and TLS, and an unusable one fails at startup. A build still reads a config whose redis url comes
from an unset variable.

Fixes #1035
…ns no task store and needs none of their secrets

The manifest boot left out the top-level redis before validating the config, but still validated sqlite,
jobs.store and tasks.redis / tasks.sqlite, so a config object whose sqlite.encryption.secret or tasks.redis.url
comes from an unset variable stopped frontmcp build, and it opened tasks.redis and created the tasks.sqlite file.
Every store is now left out before the config is validated: redis, pubsub, sqlite, jobs.store, tasks.redis and
tasks.sqlite.
@frontegg-david
frontegg-david marked this pull request as ready for review November 3, 2026 10:58
@coderabbitai

coderabbitai Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

Next included review available in 10 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration
Commits

Reviewing files that changed from the base of the PR and between 7cfc1c1 and b07bbce.

Files selected for processing: 14
  • apps/e2e/demo-e2e-cli-exec/e2e/cli-build-tasks-redis.e2e.spec.ts
  • libs/sdk/src/common/metadata/__tests__/parse-lite.spec.ts
  • libs/sdk/src/common/metadata/front-mcp.metadata.ts
  • libs/sdk/src/common/types/options/redis/schema.ts
  • libs/sdk/src/elicitation/store/__tests__/elicitation-store.factory.spec.ts
  • libs/sdk/src/elicitation/store/elicitation-store.factory.ts
  • libs/sdk/src/front-mcp/front-mcp.ts
  • libs/sdk/src/manifest/__tests__/build-manifest.boot.spec.ts
  • libs/sdk/src/scope/__tests__/scope-cli-redis.spec.ts
  • libs/sdk/src/task/store/__tests__/task-store.factory.spec.ts
  • libs/sdk/src/task/store/task-store.factory.ts
  • libs/skills/catalog/frontmcp-deployment/references/build-for-cli.md
  • libs/skills/catalog/frontmcp-deployment/references/build-manifest.md
  • libs/skills/catalog/frontmcp-setup/references/setup-redis.md
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Performance Test Results

Status: ✅ All tests passed

Summary

Project Tests Passed Warnings Failed Leaks
✅ demo-e2e-agents 4 4 0 0 0
✅ demo-e2e-cache 11 11 0 0 0
✅ demo-e2e-codecall 4 4 0 0 0
✅ demo-e2e-config 4 4 0 0 0
✅ demo-e2e-direct 3 3 0 0 0
✅ demo-e2e-elicitation 1 1 0 0 0
✅ demo-e2e-errors 4 4 0 0 0
✅ demo-e2e-hooks 3 3 0 0 0
✅ demo-e2e-multiapp 4 4 0 0 0
✅ demo-e2e-notifications 3 3 0 0 0
✅ demo-e2e-openapi 2 2 0 0 0
✅ demo-e2e-providers 4 4 0 0 0
✅ demo-e2e-public 4 4 0 0 0
✅ demo-e2e-redis 15 15 0 0 0
✅ demo-e2e-remember 4 4 0 0 0
✅ demo-e2e-remote 5 5 0 0 0
✅ demo-e2e-serverless 2 2 0 0 0
✅ demo-e2e-skills 15 15 0 0 0
✅ demo-e2e-standalone 2 2 0 0 0
✅ demo-e2e-transport-recreation 3 3 0 0 0
✅ demo-e2e-ui 4 4 0 0 0

Total: 101 tests across 21 projects

📊 View full report in workflow run


Generated at: 2026-11-03T11:14:02.074Z
Commit: 714cc4d8

@alexmercerpo
alexmercerpo merged commit fb269d0 into main Nov 3, 2026
47 checks passed
@alexmercerpo
alexmercerpo deleted the fix/h02-cli-tasks-redis-url branch November 3, 2026 12:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CLI mode: tasks ignore a redis { url } config and fall back to REDIS_URL or memory

2 participants