From bffb564508ac55668aee89a3fb7b6c795ef786ea Mon Sep 17 00:00:00 2001 From: Volker Scheuber Date: Sat, 15 Aug 2026 11:13:19 -0600 Subject: [PATCH 1/8] feat(mcp): report special-kind skill counts in server info MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The includeSpecial/allowOperationTypes gap fixed upstream in frodo-lib (applyCapabilityPolicy) was easy to get wrong silently — a preset could claim includeSpecial: true while a leftover allow-list quietly vetoed every special-kind capability anyway. `frodo mcp server info` now reports how many special-kind skills are available vs. active under the resolved policy/profile, with an active-by-risk-class breakdown, so that kind of gap is visible from the CLI instead of requiring a source read. Co-Authored-By: Claude Sonnet 5 --- src/cli/mcp/server/server-info.ts | 33 +++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/cli/mcp/server/server-info.ts b/src/cli/mcp/server/server-info.ts index 1f8a1d0b8..2c1fe1db5 100644 --- a/src/cli/mcp/server/server-info.ts +++ b/src/cli/mcp/server/server-info.ts @@ -129,10 +129,17 @@ export default function setup() { policyOverride: policySelection.policyOverride, inventoryOptions, }); - const inventoryCapabilityCount = buildCapabilityInventory( - frodo, - inventoryOptions - ).length; + const inventory = buildCapabilityInventory(frodo, inventoryOptions); + const inventoryCapabilityCount = inventory.length; + const specialInInventory = inventory.filter((c) => c.kind === 'special'); + const specialActive = service.capabilities.filter( + (c) => c.kind === 'special' + ); + const activeByRiskClass: Record = {}; + for (const capability of specialActive) { + activeByRiskClass[capability.riskClass] = + (activeByRiskClass[capability.riskClass] ?? 0) + 1; + } const info = { server: { @@ -148,6 +155,15 @@ export default function setup() { skillCounts: { inventory: inventoryCapabilityCount, active: service.capabilities.length, + special: { + // 'special' capabilities (non-CRUD, e.g. tail/evaluateScript/getTokens) + // are governed by includeSpecial rather than allowOperationTypes/ + // denyOperationTypes — surfaced explicitly here since that gate is easy + // to get wrong silently. See CapabilityPolicy.ts. + inventory: specialInInventory.length, + active: specialActive.length, + activeByRiskClass, + }, }, toolCounts: { total: service.manifest.totalToolCount, @@ -179,6 +195,15 @@ export default function setup() { printMessage( ` Active skills: ${info.service.skillCounts.active} (total: ${info.service.skillCounts.inventory})` ); + const special = info.service.skillCounts.special; + const riskBreakdown = Object.entries(special.activeByRiskClass) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([riskClass, count]) => `${count} ${riskClass}`) + .join(', '); + printMessage( + ` Active special-kind skills: ${special.active} (available: ${special.inventory})` + + (riskBreakdown ? ` — by risk: ${riskBreakdown}` : '') + ); printMessage( ` Active tools: ${info.service.toolCounts.total} (${info.service.toolCounts.canonical} canonical, ${info.service.toolCounts.discovery} discovery)` ); From 03d21c3b15e36e82c0ab80c7fd96c3dd5e4f0cbf Mon Sep 17 00:00:00 2001 From: Volker Scheuber Date: Sun, 16 Aug 2026 11:49:21 -0600 Subject: [PATCH 2/8] fix(variables): adapt to frodo-lib's resolveIdentity replacing resolvePerpetratorUuid frodo-lib renamed and restructured resolvePerpetratorUuid into resolveIdentity, which returns a structured object instead of an opaque formatted string. Add formatResolvedIdentity to rebuild an equivalent display label for the variable list/describe commands' "Modifier" column, matching the previous per-kind formatting (admin/service/realm-user) plus a new label for the "admin-unconfirmed" case that resolveIdentity can now return. --- src/ops/cloud/VariablesOps.ts | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/ops/cloud/VariablesOps.ts b/src/ops/cloud/VariablesOps.ts index 389066a32..6d9f0444f 100644 --- a/src/ops/cloud/VariablesOps.ts +++ b/src/ops/cloud/VariablesOps.ts @@ -4,6 +4,7 @@ import { VariableSkeleton, } from '@rockcarver/frodo-lib/types/api/cloud/VariablesApi'; import { VariablesExportInterface } from '@rockcarver/frodo-lib/types/ops/cloud/VariablesOps'; +import { ResolvedIdentity } from '@rockcarver/frodo-lib/types/ops/ManagedObjectOps'; import fs from 'fs'; import c from 'tinyrainbow'; @@ -30,7 +31,7 @@ const { getWorkingDirectory, saveJsonToFile, } = frodo.utils; -const { resolvePerpetratorUuid } = frodo.idm.managed; +const { resolveIdentity } = frodo.idm.managed; const { readVariables, readVariable, @@ -43,6 +44,26 @@ const { importVariables, } = frodo.cloud.variable; +/** + * Formats a resolved identity the way callers of the old + * resolvePerpetratorUuid string helper expect: a human-readable label, or the + * raw id unchanged when nothing could be resolved. + */ +function formatResolvedIdentity(identity: ResolvedIdentity): string { + switch (identity.kind) { + case 'admin': + return `Admin user: ${identity.displayName} (${identity.username})`; + case 'service': + return `Service account: ${identity.username} (${identity.displayName})`; + case 'user': + return `${identity.realm} user: ${identity.displayName} (${identity.username})`; + case 'admin-unconfirmed': + return `Tenant admin (unconfirmed): ${identity.id}`; + default: + return identity.id; + } +} + /** * List variables * @param {boolean} long Long version, all the fields besides usage @@ -119,7 +140,9 @@ export async function listVariables( wordwrap(variable.description, 40), state.getUseBearerTokenForAmApis() ? variable.lastChangedBy - : await resolvePerpetratorUuid(variable.lastChangedBy), + : formatResolvedIdentity( + await resolveIdentity(variable.lastChangedBy) + ), new Date(variable.lastChangeDate).toUTCString(), ] : [variable._id]; @@ -369,7 +392,9 @@ export async function describeVariable( ]); let modifierName: string; try { - modifierName = await resolvePerpetratorUuid(variable.lastChangedBy); + modifierName = formatResolvedIdentity( + await resolveIdentity(variable.lastChangedBy) + ); } catch { // ignore } From c9d71cde01ada8ac617f244f6287cf285e8fddb4 Mon Sep 17 00:00:00 2001 From: Volker Scheuber Date: Sun, 16 Aug 2026 15:10:27 -0600 Subject: [PATCH 3/8] docs(cli): document username-only password auto-resolution in --username help Follow-up to frodo-lib's new getTokens() behavior (roadmap item 2): passing --username alone, with a matching connection profile for the target host, now resolves that profile's stored password instead of requiring it on the command line. Updates the global --username argument's help text so this is discoverable from `--help` on any command, including `frodo mcp server start`. --- src/cli/FrodoCommand.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/FrodoCommand.ts b/src/cli/FrodoCommand.ts index 08990b58f..b72d67b45 100644 --- a/src/cli/FrodoCommand.ts +++ b/src/cli/FrodoCommand.ts @@ -443,7 +443,7 @@ const realmArgument = new Argument( const usernameArgument = new Argument( '[username]', - 'Username to login with. Must be an admin user with appropriate rights to manage authentication journeys/trees.' + "Username to login with. Must be an admin user with appropriate rights to manage authentication journeys/trees. If given without a password, and it matches the username already stored in the connection profile for the target host, frodo uses that profile's stored password instead of requiring it on the command line." ); const passwordArgument = new Argument('[password]', 'Password.'); From abd13371ed37cc8279e329e1f20847b64c841a0b Mon Sep 17 00:00:00 2001 From: Volker Scheuber Date: Sun, 16 Aug 2026 16:47:15 -0600 Subject: [PATCH 4/8] fix(mcp): drop the unused realm positional from `mcp server start` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `new FrodoCommand('frodo mcp server start', [])` kept every default positional argument, giving this command the signature ` [realm] [username] [password]` — but nothing in this command ever reads state.getRealm() afterward; it's stored and never used. That's a real footgun, and it just caused a live misconfiguration: running `frodo mcp server start ` (the natural pattern, matching `frodo info`, which explicitly omits realm the same way) put the username in the realm positional slot instead. Username ended up unset, so getTokens() fell back to loading the default stored connection profile rather than the intended one, and the stray value sitting in state as a "realm" broke session.getSessionInfo's URL construction downstream. Fixed by adding 'realm' to this command's omits, matching `info`'s existing precedent, and updating the action handler's parameter destructuring to match. Added a help example showing the username-only, password-auto-resolved invocation this footgun was tripping people up on. Verified live: `frodo mcp server start --dry-run --json` now reports authMode "admin-account" with full discovery hydration succeeding, using only host + username. --- src/cli/mcp/server/server-start.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/cli/mcp/server/server-start.ts b/src/cli/mcp/server/server-start.ts index febe18833..11487f3bb 100644 --- a/src/cli/mcp/server/server-start.ts +++ b/src/cli/mcp/server/server-start.ts @@ -8,6 +8,7 @@ import { import { Option } from 'commander'; import c from 'tinyrainbow'; +import * as s from '../../../help/SampleData'; import { MCP_LOG_LEVELS, McpLogger, @@ -62,7 +63,7 @@ type McpStartOptions = { * MCP server start command. */ export default function setup() { - const program = new FrodoCommand('frodo mcp server start', []) + const program = new FrodoCommand('frodo mcp server start', ['realm']) .description('Start an MCP server session from frodo-lib skills.') .withStability('experimental') .suppressStabilityWarning() @@ -154,12 +155,17 @@ export default function setup() { ` $ frodo mcp server start --policy read-only --profile authentication\n` ) + ` Start with selected domains only:\n` + - c.cyanBright(` $ frodo mcp server start --include-domains authn idm\n`) + c.cyanBright( + ` $ frodo mcp server start --include-domains authn idm\n` + ) + + ` Start authenticated as a username whose password is already saved in a connection profile for this host (no password on the command line):\n` + + c.cyanBright( + ` $ frodo mcp server start ${s.amBaseUrl} ${s.username}\n` + ) ) - .action(async (host, realm, username, password, options, command) => { + .action(async (host, username, password, options, command) => { command.handleDefaultArgsAndOpts( host, - realm, username, password, options, From a2d96b668aa09de24545dc08ab64cc6e160e808a Mon Sep 17 00:00:00 2001 From: Volker Scheuber Date: Sun, 16 Aug 2026 21:15:28 -0600 Subject: [PATCH 5/8] feat(build): surface build timestamps via `frodo -v` and the MCP server manifest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Companion to frodo-lib's getLibBuildTimestamp() (see that commit for the full motivation): after a source fix, tsup's app.cjs bundle can look correct in every static check — file mtimes fresh, grepped strings present — while the actually-running process still exhibits old behavior, because a downstream packaging step (or something less obvious) silently produced a stale artifact. There was no way to ask a running frodo-cli process "what did you actually get built from" without shell access to grep a binary, which itself turned out unreliable (comments don't survive bundling; -C Gzip means the packed executable isn't even readable plaintext). Added __CLI_BUILD_TIMESTAMP__, injected the same way as frodo-lib's build timestamp: a real ISO 8601 literal substituted by tsup's `define` at bundle time, not a generated source file. Exposed three ways: - `frodo -v` now prints cli-build/lib-build timestamp lines alongside the existing cli/lib version lines. - MCP_SERVER_VERSION (reported to every MCP client at protocol handshake, and by `frodo mcp server info`) now carries both build timestamps as semver build metadata (4.5.2+cli..lib.) — queryable via standard MCP protocol introspection, no shell access needed. - `frodo mcp server info` also prints/returns both as plain ISO 8601 timestamps for readability. Verified live: spawned the actual packed binary as a real MCP server via the SDK client and confirmed the handshake reports real, current timestamps matching the actual build time. Updated the one test this broke — a hardcoded exact-match on the old unversioned "Frodo MCP Server v4.5.2" info line — to check the stable version prefix plus real assertions on the new build-timestamp lines, rather than loosening it to ignore the new output. --- src/cli/mcp/server/server-info.ts | 9 +++++++++ src/ops/McpServerMetadata.ts | 23 ++++++++++++++++++++++- src/utils/Version.ts | 18 ++++++++++++++++-- test/client_cli/en/mcp-server.test.js | 7 ++++++- tsup.config.ts | 8 ++++++++ 5 files changed, 61 insertions(+), 4 deletions(-) diff --git a/src/cli/mcp/server/server-info.ts b/src/cli/mcp/server/server-info.ts index 2c1fe1db5..f5183cad5 100644 --- a/src/cli/mcp/server/server-info.ts +++ b/src/cli/mcp/server/server-info.ts @@ -14,6 +14,7 @@ import { MCP_SUPPORTED_PROTOCOL_VERSIONS, } from '../../../ops/McpServerMetadata.js'; import { printMessage } from '../../../utils/Console'; +import { getCliBuildTimestamp } from '../../../utils/Version.js'; import { FrodoStubCommand } from '../../FrodoCommand'; import { type McpPolicyPreset, resolvePolicySelection } from './server-policy'; @@ -145,6 +146,12 @@ export default function setup() { server: { name: 'Frodo MCP Server', version: MCP_SERVER_VERSION, + // Same build info baked into MCP_SERVER_VERSION's build metadata, + // spelled out as real ISO 8601 timestamps for readability — verify + // a running process actually reflects a given source change without + // needing shell access to the host it's running on. + cliBuildTimestamp: getCliBuildTimestamp(), + libBuildTimestamp: frodo.utils.version.getBuildTimestamp(), }, protocol: { supportedVersions: [...MCP_SUPPORTED_PROTOCOL_VERSIONS], @@ -187,6 +194,8 @@ export default function setup() { printMessage('MCP server info:', 'info'); printMessage(` ${info.server.name} v${info.server.version}`); + printMessage(` CLI build: ${info.server.cliBuildTimestamp}`); + printMessage(` Lib build: ${info.server.libBuildTimestamp}`); printMessage( ` Supported protocol versions: ${info.protocol.supportedVersions.join(', ')}` ); diff --git a/src/ops/McpServerMetadata.ts b/src/ops/McpServerMetadata.ts index b72f22225..6eba7c554 100644 --- a/src/ops/McpServerMetadata.ts +++ b/src/ops/McpServerMetadata.ts @@ -1,9 +1,21 @@ import { SUPPORTED_PROTOCOL_VERSIONS } from '@modelcontextprotocol/server'; +import { frodo } from '@rockcarver/frodo-lib'; import packageJson from '../../package.json'; +import { getCliBuildTimestamp } from '../utils/Version'; const MCP_LATEST_PROTOCOL_VERSION = '2026-07-28'; +/** + * Compacts an ISO 8601 timestamp into a semver build-metadata-safe + * identifier (only [0-9A-Za-z-] and dot-separated segments are valid there; + * colons and the ISO string's own dots/dashes are not). + */ +function toBuildId(timestamp: string): string { + const compact = timestamp.replace(/[^0-9A-Za-z]/g, ''); + return compact || 'unknown'; +} + export const MCP_SERVER_NAME = 'frodo-mcp'; export const MCP_SERVER_DISCOVERY_INSTRUCTIONS = 'Frodo MCP server exposes a tools-first skill surface. Call frodo_discover at most once per task and trust its active target; catalog detail is only for diagnostics. Call frodo_find_skills once with concise intent, operationTypes, objectFamily when applicable, and limit 5. Unique deterministic read-only recommendations execute automatically; when execution is returned, answer from execution.data and make no further discovery calls. On Cloud and ForgeOps, semantic count execution aggregates matching realm-qualified types and returns a per-type breakdown. Ambiguous concepts return candidates and must not be guessed. Describe the chosen skill only before mutating tools.'; @@ -15,4 +27,13 @@ export const MCP_SUPPORTED_PROTOCOL_VERSIONS = [ ), ]; -export const MCP_SERVER_VERSION = packageJson.version; +/** + * Reported to every MCP client at protocol handshake ({name, version} — see + * McpServerOps.ts) and by `frodo mcp server info`. Carries both build + * timestamps as semver build metadata so a client (or an agent debugging a + * "why doesn't this behave like the source I just changed" problem) can + * verify which actual builds are running without needing shell access to + * grep or introspect the binary — standard MCP protocol introspection is + * enough. + */ +export const MCP_SERVER_VERSION = `${packageJson.version}+cli.${toBuildId(getCliBuildTimestamp())}.lib.${toBuildId(frodo.utils.version.getBuildTimestamp())}`; diff --git a/src/utils/Version.ts b/src/utils/Version.ts index 49db5147c..a10563e50 100644 --- a/src/utils/Version.ts +++ b/src/utils/Version.ts @@ -7,7 +7,21 @@ import path from 'path'; import pkg from '../../package.json'; -const { getVersion, getAllVersions } = frodo.utils.version; +const { getVersion, getBuildTimestamp, getAllVersions } = frodo.utils.version; + +declare const __CLI_BUILD_TIMESTAMP__: string; + +/** + * ISO 8601 timestamp of when this frodo-cli bundle was built, substituted as + * a literal by tsup's `define` at bundle time (see tsup.config.ts). Falls + * back to an explicit placeholder when running from raw TypeScript source + * rather than a tsup build, since no bundler ever substitutes the + * identifier there. + */ +export const getCliBuildTimestamp = (): string => + typeof __CLI_BUILD_TIMESTAMP__ !== 'undefined' + ? __CLI_BUILD_TIMESTAMP__ + : 'unknown (running from source, not a tsup build)'; const VERSION_CACHE_FILE = `${os.homedir()}/.frodo/Versions.json`; const VERSION_CHECK_INTERVAL = 86400; @@ -147,7 +161,7 @@ export async function getVersions(checkOnly: boolean) { versionString += `\ncli: v${getCliVersion()}\nlib: v${getLibVersion()}\nnode: ${ process.version - }`; + }\ncli-build: ${getCliBuildTimestamp()}\nlib-build: ${getBuildTimestamp()}`; let newVersionString = ''; if ( (usingBinary && diff --git a/test/client_cli/en/mcp-server.test.js b/test/client_cli/en/mcp-server.test.js index 0feb77623..94ef5c22a 100644 --- a/test/client_cli/en/mcp-server.test.js +++ b/test/client_cli/en/mcp-server.test.js @@ -81,8 +81,13 @@ test("'mcp server info' prints the active server summary", async () => { const stdout = await runMcpCommand('info', '--policy', 'admin'); expect(stdout).toContain( - `MCP server info:\n Frodo MCP Server v${cliVersion}\n` + `MCP server info:\n Frodo MCP Server v${cliVersion}+` ); + // Build metadata (+cli..lib.) is real and changes + // every build, so the version line above only checks the stable prefix; + // these check the human-readable ISO timestamps carrying the same info. + expect(stdout).toMatch(/ {2}CLI build: \d{4}-\d{2}-\d{2}T[\d:.]+Z\n/); + expect(stdout).toMatch(/ {2}Lib build: \d{4}-\d{2}-\d{2}T[\d:.]+Z\n/); expect(stdout).toContain( ' Supported protocol versions: 2026-07-28, 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05, 2024-10-07\n' ); diff --git a/tsup.config.ts b/tsup.config.ts index c66cf7f11..ebce29001 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -9,6 +9,14 @@ export default defineConfig({ clean: true, bundle: true, shims: true, // this will properly transpile 'import.meta.url' + // Injected as a literal at bundle time so a running process's build can + // be verified directly, rather than trusting file mtimes or a packaging + // step (tsup here, but especially the later pkg binary-packaging step) + // that can silently produce a stale artifact despite every file on disk + // looking current. See getCliBuildTimestamp in src/utils/Version.ts. + define: { + __CLI_BUILD_TIMESTAMP__: JSON.stringify(new Date().toISOString()), + }, external: [ // list all the dev dependencies, which do NOT need to be bundled. '@types/colors', From 897f9c7b2847eadb3d09366f5720c6112db7656c Mon Sep 17 00:00:00 2001 From: Volker Scheuber Date: Sun, 16 Aug 2026 21:34:49 -0600 Subject: [PATCH 6/8] fix(mcp): disable token cache for `mcp server start`; reformat build-timestamp display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes, both from live debugging today. 1. Root-caused a real, reproducible "403 No session for request" failure on `frodo mcp server start`'s login, which appeared out of nowhere mid-session while frodo-lib's own getTokens() kept succeeding with identical credentials against the same host. The token cache (~/.frodo/TokenCache.json) is shared, on-disk, and written by every frodo process — including the four concurrent `mcp server start` processes (one per policy preset) this session normally runs, plus whatever ad-hoc test processes were spawned during today's investigation. That's exactly the kind of concurrent reader/writer pressure an unsynchronized shared cache file breaks under. Confirmed live: forcing state.setUseTokenCache(false) immediately fixed the login failure. Hard-coded off for this command specifically rather than exposed as a configurable default — the token cache exists to let successive short-lived CLI invocations skip re-authenticating, which doesn't apply to a long-running server that logs in once and relies on frodo-lib's own auto-refresh afterward. Root cause (making the on-disk cache safe for concurrent writers) is a separate, bigger piece of work, tracked for later rather than attempted here. 2. Reformatted build-timestamp display, consistently, across `frodo -v`, `frodo mcp server info`, and the MCP server manifest (the {name, version} pair every MCP client sees at protocol handshake): version number followed by the build timestamp in parentheses, e.g. `cli: v4.5.2 (2026-08-17T03:17:15.421Z)`, replacing the earlier separate "cli-build:"/"CLI build:" lines and the compacted semver build-metadata string. `mcp server info` now also shows the lib version number alongside its build timestamp, matching `frodo -v`'s existing cli/lib pairing exactly rather than showing build info without the version it belongs to. Verified live via the actual MCP protocol (a real SDK client spawning the built binary, not a hand-rolled harness): login now succeeds consistently; the handshake reports version "4.5.2 (2026-08-17T03:32:...)" in the new format; and — closing the loop on today's earlier ranking investigation — with a real request finally getting through, mutating skills (create/delete/relationship-write) no longer crowd the top of "authenticated identity info" results at all, confirming that fix does work in the real bundled binary. session.getSessionInfo now ranks 21st of 51 real candidates for that query — a genuine improvement from being unfindable, even though it still loses to the ten idm.managed.* read skills that legitimately earn the identity bonus for a query that generic. Further ranking tuning, if wanted, is a separate follow-up. --- src/cli/mcp/server/server-info.ts | 30 +++++++++++++++++++-------- src/cli/mcp/server/server-start.ts | 12 +++++++++++ src/ops/McpServerMetadata.ts | 27 ++++++++---------------- src/utils/Version.ts | 4 ++-- test/client_cli/en/mcp-server.test.js | 14 ++++++------- 5 files changed, 51 insertions(+), 36 deletions(-) diff --git a/src/cli/mcp/server/server-info.ts b/src/cli/mcp/server/server-info.ts index f5183cad5..0c1ae954b 100644 --- a/src/cli/mcp/server/server-info.ts +++ b/src/cli/mcp/server/server-info.ts @@ -9,6 +9,7 @@ import { import { Option } from 'commander'; import c from 'tinyrainbow'; +import packageJson from '../../../../package.json'; import { MCP_SERVER_VERSION, MCP_SUPPORTED_PROTOCOL_VERSIONS, @@ -145,13 +146,20 @@ export default function setup() { const info = { server: { name: 'Frodo MCP Server', + // MCP_SERVER_VERSION already carries the cli build timestamp (the + // format handshake/introspecting MCP clients see); cli/lib below + // spell out both build timestamps in the same `frodo -v` format — + // verify a running process actually reflects a given source change + // without needing shell access to the host it's running on. version: MCP_SERVER_VERSION, - // Same build info baked into MCP_SERVER_VERSION's build metadata, - // spelled out as real ISO 8601 timestamps for readability — verify - // a running process actually reflects a given source change without - // needing shell access to the host it's running on. - cliBuildTimestamp: getCliBuildTimestamp(), - libBuildTimestamp: frodo.utils.version.getBuildTimestamp(), + cli: { + version: packageJson.version, + buildTimestamp: getCliBuildTimestamp(), + }, + lib: { + version: frodo.utils.version.getVersion(), + buildTimestamp: frodo.utils.version.getBuildTimestamp(), + }, }, protocol: { supportedVersions: [...MCP_SUPPORTED_PROTOCOL_VERSIONS], @@ -193,9 +201,13 @@ export default function setup() { } printMessage('MCP server info:', 'info'); - printMessage(` ${info.server.name} v${info.server.version}`); - printMessage(` CLI build: ${info.server.cliBuildTimestamp}`); - printMessage(` Lib build: ${info.server.libBuildTimestamp}`); + printMessage(` ${info.server.name}`); + printMessage( + ` cli: v${info.server.cli.version} (${info.server.cli.buildTimestamp})` + ); + printMessage( + ` lib: v${info.server.lib.version} (${info.server.lib.buildTimestamp})` + ); printMessage( ` Supported protocol versions: ${info.protocol.supportedVersions.join(', ')}` ); diff --git a/src/cli/mcp/server/server-start.ts b/src/cli/mcp/server/server-start.ts index 11487f3bb..e71ad0566 100644 --- a/src/cli/mcp/server/server-start.ts +++ b/src/cli/mcp/server/server-start.ts @@ -171,6 +171,18 @@ export default function setup() { options, command ); + // The token cache exists to let successive short-lived CLI invocations + // reuse tokens instead of re-authenticating every time — not relevant + // to a long-running MCP server, which logs in once and relies on + // frodo-lib's own auto-refresh for the rest of its lifetime. Worse, + // it's actively unsafe here: multiple `mcp server start` processes + // (one per policy/profile) commonly run concurrently against the same + // host, all reading and writing the same on-disk token cache file — + // a real corruption/collision risk this command should never + // participate in. Hard-coded off, not exposed as a configurable + // default, until that on-disk cache is made safe for concurrent + // writers (tracked separately). + state.setUseTokenCache(false); const opts = options as McpStartOptions; if (opts.json && !opts.dryRun) { diff --git a/src/ops/McpServerMetadata.ts b/src/ops/McpServerMetadata.ts index 6eba7c554..67241e910 100644 --- a/src/ops/McpServerMetadata.ts +++ b/src/ops/McpServerMetadata.ts @@ -1,21 +1,10 @@ import { SUPPORTED_PROTOCOL_VERSIONS } from '@modelcontextprotocol/server'; -import { frodo } from '@rockcarver/frodo-lib'; import packageJson from '../../package.json'; import { getCliBuildTimestamp } from '../utils/Version'; const MCP_LATEST_PROTOCOL_VERSION = '2026-07-28'; -/** - * Compacts an ISO 8601 timestamp into a semver build-metadata-safe - * identifier (only [0-9A-Za-z-] and dot-separated segments are valid there; - * colons and the ISO string's own dots/dashes are not). - */ -function toBuildId(timestamp: string): string { - const compact = timestamp.replace(/[^0-9A-Za-z]/g, ''); - return compact || 'unknown'; -} - export const MCP_SERVER_NAME = 'frodo-mcp'; export const MCP_SERVER_DISCOVERY_INSTRUCTIONS = 'Frodo MCP server exposes a tools-first skill surface. Call frodo_discover at most once per task and trust its active target; catalog detail is only for diagnostics. Call frodo_find_skills once with concise intent, operationTypes, objectFamily when applicable, and limit 5. Unique deterministic read-only recommendations execute automatically; when execution is returned, answer from execution.data and make no further discovery calls. On Cloud and ForgeOps, semantic count execution aggregates matching realm-qualified types and returns a per-type breakdown. Ambiguous concepts return candidates and must not be guessed. Describe the chosen skill only before mutating tools.'; @@ -29,11 +18,13 @@ export const MCP_SUPPORTED_PROTOCOL_VERSIONS = [ /** * Reported to every MCP client at protocol handshake ({name, version} — see - * McpServerOps.ts) and by `frodo mcp server info`. Carries both build - * timestamps as semver build metadata so a client (or an agent debugging a - * "why doesn't this behave like the source I just changed" problem) can - * verify which actual builds are running without needing shell access to - * grep or introspect the binary — standard MCP protocol introspection is - * enough. + * McpServerOps.ts) and by `frodo mcp server info`. Carries the CLI build + * timestamp in parentheses, matching `frodo -v`'s `cli: vX (timestamp)` + * format, so a client (or an agent debugging a "why doesn't this behave + * like the source I just changed" problem) can verify which build is + * actually running without needing shell access to grep or introspect the + * binary — standard MCP protocol introspection is enough. The lib build + * timestamp — a dependency's build, not this server's own — is available + * via `frodo mcp server info` rather than crammed into this single field. */ -export const MCP_SERVER_VERSION = `${packageJson.version}+cli.${toBuildId(getCliBuildTimestamp())}.lib.${toBuildId(frodo.utils.version.getBuildTimestamp())}`; +export const MCP_SERVER_VERSION = `${packageJson.version} (${getCliBuildTimestamp()})`; diff --git a/src/utils/Version.ts b/src/utils/Version.ts index a10563e50..12fb36bf7 100644 --- a/src/utils/Version.ts +++ b/src/utils/Version.ts @@ -159,9 +159,9 @@ export async function getVersions(checkOnly: boolean) { : 'NPM package' }.`; - versionString += `\ncli: v${getCliVersion()}\nlib: v${getLibVersion()}\nnode: ${ + versionString += `\ncli: v${getCliVersion()} (${getCliBuildTimestamp()})\nlib: v${getLibVersion()} (${getBuildTimestamp()})\nnode: ${ process.version - }\ncli-build: ${getCliBuildTimestamp()}\nlib-build: ${getBuildTimestamp()}`; + }`; let newVersionString = ''; if ( (usingBinary && diff --git a/test/client_cli/en/mcp-server.test.js b/test/client_cli/en/mcp-server.test.js index 94ef5c22a..acf214c87 100644 --- a/test/client_cli/en/mcp-server.test.js +++ b/test/client_cli/en/mcp-server.test.js @@ -80,14 +80,14 @@ afterAll(() => { test("'mcp server info' prints the active server summary", async () => { const stdout = await runMcpCommand('info', '--policy', 'admin'); - expect(stdout).toContain( - `MCP server info:\n Frodo MCP Server v${cliVersion}+` + expect(stdout).toContain('MCP server info:\n Frodo MCP Server\n'); + // Build timestamps are real and change every build, so these check the + // stable `cli: vX (` / `lib: vY (` prefix plus a real ISO 8601 value + // rather than an exact match. + expect(stdout).toMatch( + new RegExp(` {2}cli: v${cliVersion} \\(\\d{4}-\\d{2}-\\d{2}T[\\d:.]+Z\\)\\n`) ); - // Build metadata (+cli..lib.) is real and changes - // every build, so the version line above only checks the stable prefix; - // these check the human-readable ISO timestamps carrying the same info. - expect(stdout).toMatch(/ {2}CLI build: \d{4}-\d{2}-\d{2}T[\d:.]+Z\n/); - expect(stdout).toMatch(/ {2}Lib build: \d{4}-\d{2}-\d{2}T[\d:.]+Z\n/); + expect(stdout).toMatch(/ {2}lib: v[\d.]+ \(\d{4}-\d{2}-\d{2}T[\d:.]+Z\)\n/); expect(stdout).toContain( ' Supported protocol versions: 2026-07-28, 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05, 2024-10-07\n' ); From 1b33cefebe8253b6d7bf58ae1bd5588ca61b2f0c Mon Sep 17 00:00:00 2001 From: Volker Scheuber Date: Sun, 16 Aug 2026 21:48:49 -0600 Subject: [PATCH 7/8] fix(mcp): hide --no-cache/--flush-cache from mcp server start help MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to hard-coding the token cache off for this command: the flags were still registered and shown in --help, implying a choice that no longer exists — --no-cache would silently be a no-op since the cache is already off, and --flush-cache would flush a cache this command never reads from or writes to in the first place. Omitted both, matching the existing pattern already used for 'realm'. --- src/cli/mcp/server/server-start.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/cli/mcp/server/server-start.ts b/src/cli/mcp/server/server-start.ts index e71ad0566..e3ea862ae 100644 --- a/src/cli/mcp/server/server-start.ts +++ b/src/cli/mcp/server/server-start.ts @@ -63,7 +63,14 @@ type McpStartOptions = { * MCP server start command. */ export default function setup() { - const program = new FrodoCommand('frodo mcp server start', ['realm']) + // 'no-cache'/'flush-cache': the token cache is hard-coded off for this + // command (see below) — showing these flags in --help would suggest a + // choice that doesn't actually exist here. + const program = new FrodoCommand('frodo mcp server start', [ + 'realm', + 'no-cache', + 'flush-cache', + ]) .description('Start an MCP server session from frodo-lib skills.') .withStability('experimental') .suppressStabilityWarning() From c76e2a42534a7c7c74487979befb318727977571 Mon Sep 17 00:00:00 2001 From: Volker Scheuber Date: Mon, 17 Aug 2026 22:38:59 -0600 Subject: [PATCH 8/8] chore: update @rockcarver/frodo-lib to version 4.3.2 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 76495fc88..6adfd4c1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@modelcontextprotocol/client": "^2.0.0", "@modelcontextprotocol/node": "^2.0.0", "@modelcontextprotocol/server": "^2.0.0", - "@rockcarver/frodo-lib": "4.3.1", + "@rockcarver/frodo-lib": "4.3.2", "@types/colors": "^1.2.1", "@types/fs-extra": "^11.0.1", "@types/jest": "^29.2.3", @@ -1908,9 +1908,9 @@ } }, "node_modules/@rockcarver/frodo-lib": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@rockcarver/frodo-lib/-/frodo-lib-4.3.1.tgz", - "integrity": "sha512-CRPaQgJExqHJ8cBgfj3BbGoggPgq0uQwUmITnHingMlu7PqGnYf3qrFaHRlRoulXwOxpgIPGIKzr9dH7pC7hqg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@rockcarver/frodo-lib/-/frodo-lib-4.3.2.tgz", + "integrity": "sha512-lcmmakt7rQnr823fwj27STQPsOy2o/AR9BGiWm/nP7hyUKSpkjtyigV7ECZeJuRwgcpxsko1Gvs6mYKkp+y4zA==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index 0795a910d..3bc4f59d7 100644 --- a/package.json +++ b/package.json @@ -119,7 +119,7 @@ "@modelcontextprotocol/client": "^2.0.0", "@modelcontextprotocol/node": "^2.0.0", "@modelcontextprotocol/server": "^2.0.0", - "@rockcarver/frodo-lib": "4.3.1", + "@rockcarver/frodo-lib": "4.3.2", "@types/colors": "^1.2.1", "@types/fs-extra": "^11.0.1", "@types/jest": "^29.2.3",