Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.3.4] - 2026-06-28

### Changed
- `generate-skills` now emits a slim front `SKILL.md` (progressive disclosure): a routing index of tool *groups* linking to `references/*.md`, instead of inlining a full per-tool description table. Keeps the front skill under the 300-token target for large services (Open Brain dropped from ~1100 tokens). Per-tool detail remains in the reference files.
- `parseExistingTools` reads tool names from the new group-index and flat-fallback formats (legacy quick-reference table still parsed for migration), so `--diff` and drift detection keep working.

## [0.3.1] - 2026-06-09

### Added
Expand Down
57 changes: 45 additions & 12 deletions src/cli/commands/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@
* Handle `mcp2cli cache <subcommand>` -- manage schema cache.
* Supports: clear [service], status, diff <service>
*/
import { clearCache, listCachedServices, readCacheRaw, detectDrift, resolveTtlMs, writeCache } from "../../cache/index.ts";
import {
clearCache,
listCachedServices,
readCacheRaw,
detectDrift,
resolveTtlMs,
writeCache,
} from "../../cache/index.ts";
import { loadConfig } from "../../config/index.ts";
import { EXIT_CODES } from "../../types/index.ts";
import type { CommandHandler } from "../../types/index.ts";
Expand Down Expand Up @@ -36,7 +43,9 @@ export const handleCache: CommandHandler = async (args: string[]) => {
" warm [service] Fetch and cache schemas (all or specific service)",
].join("\n"),
);
process.exitCode = subcommand ? EXIT_CODES.VALIDATION : EXIT_CODES.SUCCESS;
process.exitCode = subcommand
? EXIT_CODES.VALIDATION
: EXIT_CODES.SUCCESS;
break;
}
};
Expand Down Expand Up @@ -68,7 +77,9 @@ async function handleCacheDiff(args: string[]): Promise<void> {

const cached = await readCacheRaw(serviceName);
if (!cached) {
console.log(`No cached schemas for "${serviceName}". Run a command against this service first to populate the cache.`);
console.log(
`No cached schemas for "${serviceName}". Run a command against this service first to populate the cache.`,
);
process.exitCode = EXIT_CODES.SUCCESS;
return;
}
Expand All @@ -82,16 +93,28 @@ async function handleCacheDiff(args: string[]): Promise<void> {
}

try {
const { cachedSchemas: liveSchemas } = await discoverServiceSchemas(serviceName, service, { fresh: true });
const drift = detectDrift(serviceName, cached.tools, liveSchemas, cached.metadata.cachedAt);
const { cachedSchemas: liveSchemas } = await discoverServiceSchemas(
serviceName,
service,
{ fresh: true },
);
const drift = detectDrift(
serviceName,
cached.tools,
liveSchemas,
cached.metadata.cachedAt,
);

if (!drift.hasDrift) {
console.log(`No schema drift detected for "${serviceName}". Cache is current.`);
console.log(
`No schema drift detected for "${serviceName}". Cache is current.`,
);
} else {
const lines = [`Schema drift detected for "${serviceName}":\n`];
for (const change of drift.changes) {
const detail = change.details ? ` (${change.details})` : "";
const symbol = change.type === "added" ? "+" : change.type === "removed" ? "-" : "~";
const symbol =
change.type === "added" ? "+" : change.type === "removed" ? "-" : "~";
lines.push(` ${symbol} ${change.tool}${detail}`);
}
lines.push(`\nCached at: ${drift.cachedAt}`);
Expand Down Expand Up @@ -132,12 +155,19 @@ async function handleCacheWarm(args: string[]): Promise<void> {
try {
const result = await Promise.race([
(async () => {
const { cachedSchemas } = await discoverServiceSchemas(serviceName, service, { fresh: true });
const { cachedSchemas } = await discoverServiceSchemas(
serviceName,
service,
{ fresh: true },
);
await writeCache(serviceName, cachedSchemas, resolveTtlMs());
return cachedSchemas.length;
})(),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`timed out after ${PER_SERVICE_TIMEOUT}ms`)), PER_SERVICE_TIMEOUT),
setTimeout(
() => reject(new Error(`timed out after ${PER_SERVICE_TIMEOUT}ms`)),
PER_SERVICE_TIMEOUT,
),
),
]);
console.log(` ${serviceName}: ${result} tools cached`);
Expand All @@ -149,7 +179,9 @@ async function handleCacheWarm(args: string[]): Promise<void> {
}
}

console.log(`\nWarmed ${warmed} service${warmed === 1 ? "" : "s"}${failed > 0 ? `, ${failed} failed` : ""}`);
console.log(
`\nWarmed ${warmed} service${warmed === 1 ? "" : "s"}${failed > 0 ? `, ${failed} failed` : ""}`,
);
process.exitCode = EXIT_CODES.SUCCESS;
}

Expand All @@ -168,8 +200,9 @@ async function handleCacheStatus(): Promise<void> {
const entry = await readCacheRaw(service);
if (entry) {
const age = Date.now() - new Date(entry.metadata.cachedAt).getTime();
const ageHours = Math.round(age / (1000 * 60 * 60) * 10) / 10;
const ttlHours = Math.round(entry.metadata.ttlMs / (1000 * 60 * 60) * 10) / 10;
const ageHours = Math.round((age / (1000 * 60 * 60)) * 10) / 10;
const ttlHours =
Math.round((entry.metadata.ttlMs / (1000 * 60 * 60)) * 10) / 10;
const expired = age > entry.metadata.ttlMs;
const status = expired ? " (expired)" : "";
lines.push(
Expand Down
46 changes: 35 additions & 11 deletions src/cli/commands/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ async function handleSet(args: string[]): Promise<void> {
const identity = args[0];
const service = args[1];
if (!identity || !service) {
console.log("Usage: mcp2cli credentials set <identity> <service> --header 'Key: Value' [--env 'KEY=VALUE']");
console.log(
"Usage: mcp2cli credentials set <identity> <service> --header 'Key: Value' [--env 'KEY=VALUE']",
);
return;
}
const credential = parseCredentialFlags(args.slice(2));
Expand All @@ -98,7 +100,9 @@ async function handleSet(args: string[]): Promise<void> {
async function handleSetDefault(args: string[]): Promise<void> {
const service = args[0];
if (!service) {
console.log("Usage: mcp2cli credentials set-default <service> --header 'Key: Value' [--env 'KEY=VALUE']");
console.log(
"Usage: mcp2cli credentials set-default <service> --header 'Key: Value' [--env 'KEY=VALUE']",
);
return;
}
const credential = parseCredentialFlags(args.slice(1));
Expand Down Expand Up @@ -164,7 +168,9 @@ async function handleGroup(args: string[]): Promise<void> {
const name = args[1];
const members = args.slice(2);
if (!name || members.length === 0) {
console.log("Usage: mcp2cli credentials group add <name> <member1> [member2...]");
console.log(
"Usage: mcp2cli credentials group add <name> <member1> [member2...]",
);
return;
}
const result = await fetchDaemonApi("POST", "/api/credentials/groups", {
Expand All @@ -178,7 +184,9 @@ async function handleGroup(args: string[]): Promise<void> {
const name = args[1];
const members = args.slice(2);
if (!name || members.length === 0) {
console.log("Usage: mcp2cli credentials group add-members <name> <member1> [member2...]");
console.log(
"Usage: mcp2cli credentials group add-members <name> <member1> [member2...]",
);
return;
}
const result = await fetchDaemonApi(
Expand Down Expand Up @@ -206,7 +214,9 @@ async function handleGroup(args: string[]): Promise<void> {
const name = args[1];
const members = args.slice(2);
if (!name || members.length === 0) {
console.log("Usage: mcp2cli credentials group remove-members <name> <member1> [member2...]");
console.log(
"Usage: mcp2cli credentials group remove-members <name> <member1> [member2...]",
);
return;
}
const result = await fetchDaemonApi(
Expand Down Expand Up @@ -247,14 +257,17 @@ async function handleBootstrapOpenBrain(args: string[]): Promise<void> {
const desired = buildOpenBrainCredentialsFromVaultwarden(lookup.result, {
serviceName: options.service,
});
const existing = await fetchDaemonApi("GET", "/api/credentials") as {
const existing = (await fetchDaemonApi("GET", "/api/credentials")) as {
credentials?: Record<string, Record<string, unknown>>;
};
const changed: string[] = [];
const skipped: string[] = [];

for (const entry of desired) {
if (!options.force && existing.credentials?.[entry.identity]?.[entry.service]) {
if (
!options.force &&
existing.credentials?.[entry.identity]?.[entry.service]
) {
skipped.push(entry.identity);
continue;
}
Expand All @@ -266,10 +279,16 @@ async function handleBootstrapOpenBrain(args: string[]): Promise<void> {
changed.push(entry.identity);
}

console.log(JSON.stringify(formatOpenBrainBootstrapSummary(options, desired, changed, skipped)));
console.log(
JSON.stringify(
formatOpenBrainBootstrapSummary(options, desired, changed, skipped),
),
);
}

function parseBootstrapOpenBrainFlags(args: string[]): { item: string; service: string; force: boolean } | null {
function parseBootstrapOpenBrainFlags(
args: string[],
): { item: string; service: string; force: boolean } | null {
const options = {
item: "Open Brain - Per-User Tokens",
service: "open-brain",
Expand All @@ -285,7 +304,9 @@ function parseBootstrapOpenBrainFlags(args: string[]): { item: string; service:
} else if (arg === "--force") {
options.force = true;
} else {
console.log("Usage: mcp2cli credentials bootstrap-open-brain [--item 'Open Brain - Per-User Tokens'] [--service open-brain] [--force]");
console.log(
"Usage: mcp2cli credentials bootstrap-open-brain [--item 'Open Brain - Per-User Tokens'] [--service open-brain] [--force]",
);
return null;
}
}
Expand Down Expand Up @@ -352,7 +373,10 @@ function parseCredentialFlags(
return null;
}

const result: { headers?: Record<string, string>; env?: Record<string, string> } = {};
const result: {
headers?: Record<string, string>;
env?: Record<string, string>;
} = {};
if (Object.keys(headers).length > 0) result.headers = headers;
if (Object.keys(env).length > 0) result.env = env;
return result;
Expand Down
Loading