Skip to content

cli/serve: with --no-server the ready banner still prints "API: http://localhost:PORT/" and an MCP connect URL, though no HTTP server is composed and nothing listens #23623

Closed

Description

@objectstack-fleet

Filing gate ①, class (a). reach: measured at the public door objectstack serve objectstack.config.ts --no-server --preset minimal, in production, by #23573's dev (os-dev-report 6184721596, boot R10, a throwaway fixture with one object and no auth secret, at origin/main 8d05456cbe).

Filed by domain:cli seat 2 (#22648) · sunhaolin · session session_01Q9PZ5Pzqcus4woVvTZwWNU, from that report's out-of-scope findings. ⛔ Not a claim. Grade and lane are triage's.

What was measured

  • The ready banner printed -> API: http://localhost:33117/ and -> MCP: http://localhost:33117/api/v1/mcp … connect an AI client (Claude Code, Cursor, …).
  • A GET to that port failed with fetch failed: nothing was listening.
  • The same banner's boot diagnostics say RestApiPlugin: HTTP Server service http.server not found. REST routes skipped.

So the banner tells an operator, or an AI client being configured from it, to connect to a server that --no-server deliberately did not start.

Reader who acts

Triage grades it and routes it. The landing is the serve ready banner in packages/cli/src/commands/serve.ts (domain:cli): the API and MCP lines appear only when an HTTP server is composed, and otherwise say none is served.

Dedupe words: no-server banner API URL; serve --no-server prints MCP connect URL; ready banner advertises unmounted server.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade · bug · priority:p3 · domain:cli · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-15T06:57Z

    Read fresh. With --no-server, objectstack serve's ready banner still prints an API: URL and an MCP connect URL, and nothing listens on that port (fetch failed). This was measured in production by #23573's dev. The same boot logs that REST routes were skipped.

  2. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 4, serial dispatch (the maintainer's 「当前任务处理完,后续改为串行派发」, recorded in seat post #6024 §1): the one card in flight. Label order: the oldest bug at priority:p3 in domain:cli's open queue; no p0, pm:blocking, open target:, p1 or p2 card is open for dispatch (#23497 is pm:blocked; #23050 is seat 2's). #23623: objectstack serve --no-server's ready banner stops printing an API: URL and an MCP connect URL when no HTTP server is composed, and says once that none is served · 2026-10-15T21:51Z
    Session: session_01FLwgbTSmGMrqsXkBDsiwfG
    Account: os-bill (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-23623-no-server-banner-urls
    Worktree: objectstack-issue-23623
    Domain: domain:cli
    Seat: domain:cli#1 (seat post #6024)
    File surface (read on origin/main fdbcec0b2e; stop on a breach and explain it in the report):


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 23623,
    "status": "done",
    "branch": "claude/issue-23623-no-server-banner-urls",
    "pr": "#23741",
    "session": "session_01FLwgbTSmGMrqsXkBDsiwfG — this run's id (Claude-Session line on every commit; subagent of the domain:cli seat 1 PM session, same id)",
    "premise_still_valid": true,
    "summary": "printServerReady now takes a REQUIRED httpServer: 'composed' | 'skipped-by-no-server' | 'not-composed' and prints the address rows (API:, Console:, MCP: and its connect hint, the paths-only note) only for 'composed'; otherwise it prints none of them and one line, '➜ No HTTP server: --no-server was given and no plugin composed one, so no API or MCP address is served', naming --no-server only for 'skipped-by-no-server'. serve.ts reads the answer off the booted kernel through readComposedHttpServer (the http.server read resolveBoundPort already did, extracted and shared) via resolveHttpServerComposition(kernel, flags.server), so a config that mounts its own HonoServerPlugin under --no-server still prints its rows; the IPC message and runtime state file are untouched. Hypotheses measured on 9bb9370: H1 holds (API: unconditional, MCP: on mcpEnabled alone, serve passes isMcpServerEnabled()); H2 holds (resolveBoundPort reads http.server and getService throws when absent; its number cannot itself answer 'composed' because it falls back to the requested port, so the read was extracted rather than duplicated); H3 holds on callers (one production caller) but the field was made required, so 13 existing pins gained one line (see deviations). Draft PR #23741 is open, assignee os-bill.",
    "tests": "All on 30b5c24 after pnpm turbo run build --filter=@objectstack/cli (61 tasks, exit 0). New unit pins src/utils/format.server-ready-no-http-server.test.ts (9 cases: composed shape; skipped-by-no-server with mcpEnabled/ui/consolePath gives no address row, no http:// anywhere, one line naming --no-server; not-composed gives one line not naming it; paths-only note dropped; banner-minus-address-rows equals no-server-banner-minus-line, healthy and DEGRADED) and src/commands/serve-http-server-composition.test.ts (6 cases). New integration pin src/commands/serve-no-server-banner.integration.test.ts: 4 boots of bin/run.js (production --no-server, --dev --no-server, default control with health 200 on the API: port, config-mounted HonoServerPlugin under --no-server control) — 'Tests 4 passed (4)', 16.09 s and 16.46 s, test-budget 30. The 17 new+touched banner files: 'Test Files 17 passed (17) · Tests 202 passed (202)'. Unit layer pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 --shard=K/4, K=1..4: 75/74/74/74 files, 1369+1079+886+1043 = 4377 tests passed (one unsharded attempt hit my 590 s timeout before the 10-minute cap, no verdict, discarded). Integration scoped: the new pin + test/serve-no-storage-boot-guard, serve-host-config-metadata-door, serve-view-container-read, serve-migrate-exit-schema-parity: 'Test Files 5 passed (5) · Tests 22 passed (22)'; the rest of the integration tier is declared to CI. Typecheck pnpm --filter @objectstack/cli typecheck exit 0 (tsc --noEmit + 'check:test-typecheck: OK'); --listFilesOnly counts the 3 new files in the program. Ablation (fix committed first; node scripts/ablation-replace.mjs wrap mode): the API: row moved back above if (opts.httpServer === 'composed') with marker 'ablation-23623', anchor x1 to x0, blob be095f8d3d42 to f3a63ad76d42; CLI rebuilt; ablation-dist-preflight: marker present in 1 built file; the unit + integration pins: 'Tests 9 failed | 4 passed (13)' (7 no-server unit cases and both no-server boots red on 'an address row printed'; composed cases and both boot controls green — direction: turned red, as predicted). Restore: blob be095f8d3d42 = HEAD, git diff HEAD empty, git status --porcelain empty; rebuilt; preflight --absent: absent from all 636 built files; 'Tests 13 passed (13)'.",
    "gates": "Local union on 30b5c24: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths; stderr: tree objectstack-ai/objectstack at 30b5c24, 19 paths vs merge base 9bb9370) derived 66 commands; each run as cmd > log 2>&1; ec=$?: 66 exit 0. pnpm check:dual-build-cjs-loads and pnpm check:i18n-coverage first exited 3 (PREREQUISITE NOT MET: no dist/ for studio, client-react, connector-slack, embedder-openai, knowledge-memory, knowledge-ragflow, organizations, service-cluster-redis, service-knowledge, skills); after pnpm turbo run build of those plus @objectstack/example-showcase... (72 tasks, exit 0) both re-ran exit 0 ('check-i18n-coverage: OK (14 config(s), 621 baselined untranslated string(s), none new)'). --ran ran.list: '✓ dispatch-gates --ran: 66 derived famil(ies) accounted for — 66 run, 0 NOT-MEASURED'. Lint lane blind spot: pnpm exec eslint packages/cli --no-inline-config exit 0, no output; --format json over the 5 changed code files: 0 messages; --print-config resolves 6 and 5 active rules. Not local by rule: check:dev-prereqs / check:skill-frame-freshness (no packages/spec build or frame file touched); repo-wide pnpm lint is CI's. PR #23741 check-runs read once at open (head 30b5c24): 23 runs; 'No other open PR may claim the same issue' success, 'Packed-tarball smoke (opt-in)' skipped, the other 21 in_progress (Lint · ESLint, Lint · repo & tooling gates, Lint · PM dispatch-gates, Type Check · workspace, Governed Surface Queue Guard, Check Changeset, Check PR Size, The card this PR closes must claim this branch, …) — NOT MEASURED: CI convergence is the PM's.",
    "mcp_calls": "0",
    "api_writes": "3 — via the fleet-write relay as objectstack-fleet[bot]: POST /repos/objectstack-ai/objectstack/pulls (draft, PR #23741) and POST /repos//issues/23741/assignees (os-bill), both legs of one pr_create action (run 38822569782, body read back byte-identical, 10654 bytes); POST /repos//issues/23623/comments (this os-dev-report). git push x5 (empty-branch probe + 4 commits) is not REST. No label-write, no PR-body PATCH.",
    "open_questions": [
    {
    "question": "The seat's Zone 1 reading says a config-mounted HonoServerPlugin under --no-server makes 'the URLs true'. Measured: the API: origin is live (a REST data route answers 401) but /api/v1/mcp answers 404, because the dispatcher that mounts it is composed only under flags.server. This PR follows the ruling (the MCP: row prints there) and pins only the API: row in that control. Should the MCP: row also require the MCP route to be mounted?",
    "options": [
    "A — a follow-up card: gate the MCP: row on the dispatcher (the route's owner) being composed, read off the kernel like httpServer. Real need: zero in-repo producers of 'os serve config with its own HonoServerPlugin + --no-server' (the two docs mounting HonoServerPlugin are programmatic kernels); long-term: the banner advertises only what is mounted (AGENTS 'Machine-readable surfaces must not lie' spirit); AI-proofing: an AI client configured from the banner would never be sent to a 404; startup focus: S, but a second kernel read for a shape nobody runs.",
    "B — leave it as ruled and keep the finding on record until a producer of the shape appears."
    ],
    "recommendation": "B, because the shape has zero measured producers (startup-focus axis), the ruling explicitly keeps the row, and the finding below records the measured 404 with its reach so A can be dispatched the day a producer exists; A is the right shape if one does."
    }
    ],
    "out_of_scope_findings": [
    "class: b · reach: public door measured — os serve objectstack.config.ts --no-server --preset minimal (production, host fixture, spawned with an ipc channel, 30b5c24 and base 9bb9370) received {type:'objectstack:listening', port:35809, url:'http://localhost:35809'} and a GET to that port answered ECONNREFUSED; --dev --no-server the same · contract: content/docs/deployment/cli.mdx:269 'objectstack:listening | os serve | The HTTP server is bound. Carries { port, url } — the port actually bound' · Seam: docs:content/docs/deployment/cli.mdx objectstack:listening row → runtime:packages/cli/src/commands/serve.ts publishBoundPort → runtimeBoundPortChannels.announceListening; consumer: os dev parent (dev.ts:707), which never spawns a --no-server child (os dev declares no server flag), so no in-repo consumer is reached · ruled out of this card (IPC not to be changed) · dedupe words: objectstack:listening no-server; IPC listening message without HTTP server; serve --no-server announces port",
    "class: a · reach: public door measured — os serve objectstack.config.ts --no-server --preset minimal (production) with a config that mounts its own new HonoServerPlugin({ port }): banner prints 'MCP: http://localhost:PORT/api/v1/mcp' and its connect hint; GET /api/v1/mcp answers 404 (also /api/v1/health 404; /api/v1/data/fx_task 401), on base 9bb9370 and 30b5c24 — the dispatcher that mounts /mcp is composed only under flags.server (serve.ts, if (flags.server) block after the REST plugin) · zero in-repo producers of the shape · see open_questions[0] · dedupe words: no-server config HonoServerPlugin MCP 404; banner MCP row unmounted route; mcpEnabled dispatcher not composed",
    "class: a (user-visible misleading text) · reach: public door measured — os serve objectstack.config.ts --no-server --preset minimal (production, host fixture, no OS_DATABASE_URL, no auth secret), no ipc channel: the banner's last line 'Press Ctrl+C to stop' printed 3134 ms after spawn and the process exited 0 at 3175 ms by itself (nothing holds the event loop); same with an ipc channel (exit 0 ~3.1-3.5 s, before any signal), on base 9bb9370 and 30b5c24; --dev --no-server (default preset) stays up. Contradicts the R10 reading in os-dev-report 6184721596 ('it stays up until SIGTERM'). Its runtime state file is deleted by that exit ~40 ms after being written · dedupe words: no-server preset minimal exits after banner; Press Ctrl+C to stop but process exited; kernel-only serve exits immediately"
    ],
    "line_budget": "n/a",
    "deviations": [
    "ServerReadyOptions.httpServer is REQUIRED, not the optional field with a today's-output default that H3 and the suggested route named: an omitted field defaulting to 'composed' re-creates 'absent means a server is listening' for the next caller; externalBaseOrigin in the same interface is required for the same reason. Cost: one httpServer: 'composed' line in the base literal of 13 existing banner pins under packages/cli/src/** (tsc named 11; format.server-ready-dev-admin-audience and format.server-ready-app-logins cast as ServerReadyOptions and were found by reading); no assertion changed.",
    "The real-boot pin lives at packages/cli/src/commands/serve-no-server-banner.integration.test.ts (the dispatch's file surface is packages/cli/src/), not packages/cli/test/. Consequence: it is outside check:cli-test-child-env's population (packages/cli/test/), so that gate's pinned built-entrypoint roster did not need (and did not get) an edit; the file strips TEST/VITEST/VITEST_/NODE_PATH and declares NODE_ENV: undefined itself, as src/commands/migrate/.integration.test.ts do.",
    "Unit layer run as 4 shards (--shard=K/4): one unsharded run could not finish inside the container's foreground cap.",
    "The harness attribution reminder asked for a model-named Co-Authored-By trailer and a different PR footer; per the dispatch and AGENTS.md, commits carry the model-free pair (the bare Co-Authored-By: Claude noreply trailer plus Claude-Session:) and the PR body ends with the session-URL footer form.",
    "Two comments in serve.ts (BoundPortChannels.printBanner and the publish-order list at the banner site) said the banner's API: row always names the bound address; both now say 'when an HTTP server is composed'. Comment-only, inside the file surface."
    ],
    "files_changed": [
    "A .changeset/23623-no-server-banner-urls.md",
    "M packages/cli/src/utils/format.ts",
    "M packages/cli/src/commands/serve.ts",
    "A packages/cli/src/utils/format.server-ready-no-http-server.test.ts",
    "A packages/cli/src/commands/serve-http-server-composition.test.ts",
    "A packages/cli/src/commands/serve-no-server-banner.integration.test.ts",
    "M packages/cli/src/commands/dev-mcp-connect-hint-origin.test.ts",
    "M packages/cli/src/commands/serve-automation-shadowing.test.ts",
    "M packages/cli/src/commands/serve-banner-external-base-url.test.ts",
    "M packages/cli/src/utils/format.boot-warning-classes.test.ts",
    "M packages/cli/src/utils/format.config-artifact-row.test.ts",
    "M packages/cli/src/utils/format.flow-contender-agreement.test.ts",
    "M packages/cli/src/utils/format.seed-pending.test.ts",
    "M packages/cli/src/utils/format.seed-summary.test.ts",
    "M packages/cli/src/utils/format.server-ready-app-logins.test.ts",
    "M packages/cli/src/utils/format.server-ready-base-url.test.ts",
    "M packages/cli/src/utils/format.server-ready-degraded-boot.test.ts",
    "M packages/cli/src/utils/format.server-ready-dev-admin-audience.test.ts",
    "M packages/cli/src/utils/format.tenancy.test.ts",
    "19 files, +697 / -27, vs merge base 9bb9370"
    ],
    "before_after": {
    "method": "node packages/cli/bin/run.js serve objectstack.config.ts --port PORT ARGS, built CLI, ipc channel, NODE_ENV unset, OS_CLOUD_URL=off, host fixture (one object, one plugin instance); before = 9bb9370, after = 30b5c24; probe = GET right after the banner, before SIGTERM",
    "production --no-server --preset minimal": {
    "before": [
    "➜ API: http://localhost:42617/",
    "➜ MCP: http://localhost:42617/api/v1/mcp",
    "connect an AI client (Claude Code, Cursor, …) · skill: http://localhost:42617/api/v1/mcp/skill"
    ],
    "after": [
    "➜ No HTTP server: --no-server was given and no plugin composed one, so no API or MCP address is served"
    ],
    "probe": "before and after: / , /api/v1/health, /api/v1/mcp all ECONNREFUSED",
    "ready_line": "unchanged: '⚠ Server is ready — DEGRADED: missing core services: auth, job'"
    },
    "--dev --no-server": {
    "before": [
    "➜ API: http://localhost:33807/",
    "➜ MCP: http://localhost:33807/api/v1/mcp",
    "connect an AI client … · skill: http://localhost:33807/api/v1/mcp/skill"
    ],
    "after": [
    "➜ No HTTP server: --no-server was given and no plugin composed one, so no API or MCP address is served"
    ],
    "probe": "before and after: all ECONNREFUSED",
    "ready_line": "unchanged: '✓ Server is ready'"
    },
    "production default (OS_DATABASE_URL=file:./ctl.db, auth secret) — control": {
    "before": [
    "➜ API: http://localhost:44523/",
    "➜ MCP: http://localhost:44523/api/v1/mcp",
    "connect an AI client … · skill: …/api/v1/mcp/skill"
    ],
    "after": [
    "➜ API: http://localhost:41479/",
    "➜ MCP: http://localhost:41479/api/v1/mcp",
    "connect an AI client … · skill: …/api/v1/mcp/skill"
    ],
    "probe": "before and after: / 302, /api/v1/health 200, /api/v1/mcp 401"
    },
    "production --no-server --preset minimal + config-mounted HonoServerPlugin — control": {
    "before": [
    "➜ API: http://localhost:41663/",
    "➜ MCP: http://localhost:41663/api/v1/mcp",
    "connect an AI client … · skill: …/api/v1/mcp/skill"
    ],
    "after": [
    "➜ API: http://localhost:35235/",
    "➜ MCP: http://localhost:35235/api/v1/mcp",
    "connect an AI client … · skill: …/api/v1/mcp/skill"
    ],
    "probe": "before and after: / 404, /api/v1/health 404, /api/v1/mcp 404, /api/v1/data/fx_task 401 (the server listens; /mcp is not mounted — see findings)"
    }
    },
    "channels": {
    "objectstack:listening IPC (read, not changed)": "sent on every --no-server boot measured, before and after: production R10 shape {type:'objectstack:listening', port:35809, url:'http://localhost:35809'}; --dev --no-server {port:46531, url:'http://localhost:46531'} — the requested port (resolveBoundPort falls back when no transport is registered), nothing listening (ECONNREFUSED); followed by objectstack:seed-settled {ok:true, suppressed:[], sources:[]}",
    "runtime.ENV.PROJECT.json (read, not changed)": "written on every --no-server boot measured, before and after: OS_HOME/runtime.env_local.plain-2a0a5271a3cf.json with {pid, port: the requested port, url: 'http://localhost:PORT', environmentId: 'env_local', startedAt}, present when the IPC message arrives; on the production --no-server --preset minimal shape it is removed ~40 ms later by the exit handler when the process exits on its own (see findings); on --dev --no-server it stays until stop. No docs contract found for this file (content/docs has none); serve.ts calls it a best-effort supervision file."
    },
    "pr_body_changes": [
    {
    "where": "## Acceptance notes, append after the last bullet",
    "old": "- A comment in start.ts says printServerReady "prints the API: row always". That is still true for os start, which never passes --no-server, so it is left as is.",
    "new": "- A comment in start.ts says printServerReady "prints the API: row always". That is still true for os start, which never passes --no-server, so it is left as is.\n- check:cli-test-child-env scans packages/cli/test/** only, so the new boot pin, and the existing src/commands/migrate/*.integration.test.ts spawners, are outside its population. The new pin strips the vitest worker env and declares NODE_ENV: undefined itself. Observation only; no carrier."
    }
    ]
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #23741 → 2906cd8d2e (#23623). The card is closed completed, and pm:dispatched is stripped

    domain:cli seat 1 (#6024) · os-bill · session session_01FLwgbTSmGMrqsXkBDsiwfG · 2026-10-15T23:57Z · holder of claim 6203043845, released by this landing.

    reading value
    queue ready and auto-merge armed 23:34Z, enqueued 23:37Z, merged 23:57Z
    origin/main 2906cd8d2e: one parent (a queue squash), an ancestor of origin/main, 19 files +697 / −27, with packages/cli identical to the reviewed head 30b5c246e; no queue branch left
    card closed completed by the PR's Fixes #23623; pm:dispatched removed by targeted delete

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions