Skip to content
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file. The format

## [Unreleased]

## [1.1.1] - 2026-08-12

### Changed

- [#399](https://github.com/mohanagy/miftah/issues/399) Prepared the compatible v1.1.1 patch release for Claude Desktop tool-catalog compatibility. Publication remains gated on exact `development`-to-`main` promotion and protected OIDC trusted publishing, registry provenance, a fresh install, and package-signature verification.

### Fixed

- [#397](https://github.com/mohanagy/miftah/issues/397) Preserved valid JSON Schema objects when tool input names look credential-related, so Vercel and Firebase catalogs no longer fail MCP client validation after redaction. Client-visible schema-valued `true` is emitted as its equivalent `{}` form for Claude Desktop proxy compatibility, while `false` constraints, ordinary boolean values, configured-secret redaction, bearer redaction, and provider-token redaction remain unchanged.

## [1.1.0] - 2026-08-12

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Install Miftah, then choose the terminal wizard or the browser Console. Both use
### 1. Install the current release

```bash
npm install -g @lubab/miftah@1.1.0
npm install -g @lubab/miftah@1.1.1
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
miftah version
```

Expand Down
2 changes: 1 addition & 1 deletion docs/mcp-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This page is the compatibility source of truth for Miftah's downstream MCP server. It records protocol-era behavior separately from generated client-configuration support and from upstream MCP transport support. A generated snippet proves only that Miftah emitted the documented JSON shape; it does not prove that an untested host completed a protocol exchange.

- Miftah baseline: `1.1.0`
- Miftah baseline: `1.1.1`
- Locked MCP TypeScript packages: `@modelcontextprotocol/client`, `core`, `server`, `node`, and `server-legacy` `2.0.0`
- Evidence date: 2026-08-12
- Modern protocol era: `2026-07-28`
Expand Down
2 changes: 1 addition & 1 deletion docs/presets-and-clients.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ This is the compatibility source of truth for generated `miftah init` configurat
For downstream protocol eras and real packaged-host evidence, see [MCP protocol and client compatibility](mcp-compatibility.md). The tables below validate generated configuration shapes; they do not by themselves establish a runtime exchange with Claude Desktop, Claude Code, Cursor, or VS Code.

- Catalog version: `3`
- Miftah package version: `1.1.0`
- Miftah package version: `1.1.1`
- Last tested / validation boundary: the catalog builds strict Miftah configuration that `validateConfig` accepts. The docs contract test checks generated configuration only; it does **not** construct a runtime, start, authenticate to, or smoke-test external providers.

Miftah itself requires Node.js `>=20`. That does not establish an upstream server's Node requirement.
Expand Down
4 changes: 2 additions & 2 deletions docs/whats-new-in-0.5.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# What is in Miftah 0.5

Install `@lubab/miftah@1.1.0`, the current stable release, to use the guided setup and account-management capabilities introduced in 0.5 instead of assembling a multi-account configuration by hand:
Install `@lubab/miftah@1.1.1`, the current stable release, to use the guided setup and account-management capabilities introduced in 0.5 instead of assembling a multi-account configuration by hand:

```bash
npm install -g @lubab/miftah@1.1.0
npm install -g @lubab/miftah@1.1.1
miftah version
```

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lubab/miftah",
"version": "1.1.0",
"version": "1.1.1",
"description": "Wrap any MCP. Use the right account without reconnecting.",
"keywords": [
"mcp",
Expand Down
118 changes: 115 additions & 3 deletions src/mcp/server/miftah-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1071,7 +1071,10 @@ export class MiftahServer {
? tools.map(stripMcpParameterHeaderAnnotations)
: tools
};
}
},
undefined,
undefined,
(result) => redactToolListResult(result, this.redactor)
);
});

Expand Down Expand Up @@ -3052,14 +3055,15 @@ export class MiftahServer {
},
operation: (audit: AuditScope) => Promise<Result>,
errorResult?: (error: MiftahError) => Result,
resultAudit?: (result: Result) => AuditScopeResult
resultAudit?: (result: Result) => AuditScopeResult,
resultRedactor?: (result: Result) => Result
): Promise<Result> {
const audit = this.auditTrail.beginOperation(input);
try {
await this.auditTrail.ensureWritable();
const result = await operation(audit);
await audit.finish(resultAudit?.(result) ?? { status: "success" });
return this.redactor.redact(result);
return resultRedactor === undefined ? this.redactor.redact(result) : resultRedactor(result);
} catch (error) {
if (error instanceof ApprovalInputRequiredSignal) {
if (!audit.isFinalized) {
Expand Down Expand Up @@ -3641,6 +3645,114 @@ function stripMcpParameterHeaderAnnotations(tool: Tool): Tool {
};
}

const jsonSchemaMapKeywords = new Set([
"$defs",
"definitions",
"dependentSchemas",
"patternProperties",
"properties"
]);
const jsonSchemaArrayKeywords = new Set(["allOf", "anyOf", "oneOf", "prefixItems"]);
const jsonSchemaValueKeywords = new Set([
"additionalItems",
"additionalProperties",
"contains",
"contentSchema",
"else",
"if",
"items",
"not",
"propertyNames",
"then",
"unevaluatedItems",
"unevaluatedProperties"
]);

/** Redacts catalog metadata without mistaking JSON Schema property names for secret values. */
function redactToolListResult<Result extends { tools: Tool[] }>(
result: Result,
redactor: SecretRedactor
): Result {
const { tools, ...metadata } = result;
return {
...redactor.redact(metadata),
tools: tools.map((tool) => redactClientVisibleTool(tool, redactor))
} as Result;
}

function redactClientVisibleTool(tool: Tool, redactor: SecretRedactor): Tool {
const { inputSchema, outputSchema, ...metadata } = tool;
return {
...redactor.redact(metadata),
inputSchema: redactClientVisibleSchema(inputSchema, redactor),
...(outputSchema === undefined
? {}
: { outputSchema: redactClientVisibleSchema(outputSchema, redactor) })
};
}

/**
* Preserves schema structure while redacting known values from textual metadata.
* A boolean `true` in a schema position is normalized to its equivalent `{}`
* form for clients whose schema adapters require objects; `false` stays closed.
*/
function redactClientVisibleSchema<Schema>(schema: Schema, redactor: SecretRedactor): Schema {
if (schema === true) return {} as Schema;
if (schema === false || schema === null || typeof schema !== "object") {
return (typeof schema === "string" ? redactor.redact(schema) : schema) as Schema;
}
if (Array.isArray(schema)) {
return schema.map((entry) => redactClientVisibleSchema(entry, redactor)) as Schema;
}

return Object.fromEntries(
Object.entries(schema).map(([keyword, value]) => {
const redactedKeyword = redactor.redact(keyword);
if (jsonSchemaMapKeywords.has(keyword) && isRecord(value)) {
return [
redactedKeyword,
Object.fromEntries(
Object.entries(value).map(([name, nestedSchema]) => [
redactor.redact(name),
redactClientVisibleSchema(nestedSchema, redactor)
])
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
];
}
if (jsonSchemaArrayKeywords.has(keyword) && Array.isArray(value)) {
return [redactedKeyword, value.map((nestedSchema) => redactClientVisibleSchema(nestedSchema, redactor))];
}
if (jsonSchemaValueKeywords.has(keyword)) {
return [redactedKeyword, redactClientVisibleSchema(value, redactor)];
}
if (keyword === "dependencies" && isRecord(value)) {
return [
redactedKeyword,
Object.fromEntries(
Object.entries(value).map(([name, dependency]) => [
redactor.redact(name),
Array.isArray(dependency)
? redactSchemaLiteral(dependency, redactor)
: redactClientVisibleSchema(dependency, redactor)
])
)
];
}
return [redactedKeyword, redactSchemaLiteral(value, redactor)];
})
) as Schema;
}

/** Redacts strings and object keys inside non-schema keyword values without changing booleans. */
function redactSchemaLiteral<Value>(value: Value, redactor: SecretRedactor): Value {
if (typeof value === "string") return redactor.redact(value) as Value;
if (Array.isArray(value)) return value.map((entry) => redactSchemaLiteral(entry, redactor)) as Value;
if (value === null || typeof value !== "object") return value;
return Object.fromEntries(
Object.entries(value).map(([name, entry]) => [redactor.redact(name), redactSchemaLiteral(entry, redactor)])
) as Value;
}

function stripJsonSchemaKeyword<T>(value: T, keyword: string): T {
if (Array.isArray(value)) return value.map((entry) => stripJsonSchemaKeyword(entry, keyword)) as T;
if (typeof value !== "object" || value === null) return value;
Expand Down
17 changes: 8 additions & 9 deletions tests/authenticated-request-context-docs-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest";

const libraryApiPath = fileURLToPath(new URL("../docs/library-api.md", import.meta.url));
const changelogPath = fileURLToPath(new URL("../CHANGELOG.md", import.meta.url));
const packageManifestPath = fileURLToPath(new URL("../package.json", import.meta.url));
const protocolReleaseVersion = "1.1.0";

describe("authenticated request-context documentation contract", () => {
it("documents the trusted host boundary and the no-fallback compatibility path", async () => {
Expand All @@ -19,19 +19,18 @@ describe("authenticated request-context documentation contract", () => {
expect(documentation).toContain("does not synthesize verified per-chat claims");
});

it("records the additive security boundary under the package release", async () => {
it("records the additive security boundary under the v1.1.0 protocol release", async () => {
const changelog = await readFile(changelogPath, "utf8");
const manifest = JSON.parse(await readFile(packageManifestPath, "utf8")) as { version: string };
const escapedVersion = manifest.version.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
const escapedVersion = protocolReleaseVersion.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
const heading = changelog.match(new RegExp(`^## \\[${escapedVersion}\\] - \\d{4}-\\d{2}-\\d{2}$`, "mu"));
expect(heading?.index).toBeTypeOf("number");
const releaseStart = heading?.index ?? 0;
const releaseEnd = changelog.indexOf("\n## ", releaseStart + (heading?.[0].length ?? 0));
const currentRelease = changelog.slice(releaseStart, releaseEnd < 0 ? undefined : releaseEnd);
const protocolRelease = changelog.slice(releaseStart, releaseEnd < 0 ? undefined : releaseEnd);

expect(currentRelease).toContain("[#376]");
expect(currentRelease).toContain("for modern stateless handling");
expect(currentRelease).toContain("never falls back to MCP `clientInfo`");
expect(currentRelease).toContain("embedding hosts supply them through the public server factory");
expect(protocolRelease).toContain("[#376]");
expect(protocolRelease).toContain("for modern stateless handling");
expect(protocolRelease).toContain("never falls back to MCP `clientInfo`");
expect(protocolRelease).toContain("embedding hosts supply them through the public server factory");
});
});
22 changes: 11 additions & 11 deletions tests/fixtures/fake-upstream-bundled.mjs

Large diffs are not rendered by default.

60 changes: 60 additions & 0 deletions tests/fixtures/fake-upstream-runtime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const hangOnStartReadyPath = process.env.TEST_HANG_ON_START_READY_PATH;
const shutdownDelayMs = Number(process.env.TEST_SHUTDOWN_DELAY_MS ?? "0");
const shutdownEndPath = process.env.TEST_SHUTDOWN_END_PATH;
const includeIdentityTool = process.env.TEST_INCLUDE_IDENTITY_TOOL === "true";
const includeSchemaCompatibilityTools = process.env.TEST_INCLUDE_SCHEMA_COMPAT_TOOLS === "true";
const oversizedIdentityResponseRepeat = Number(process.env.TEST_OVERSIZED_IDENTITY_RESPONSE_REPEAT ?? "0");
const oversizedIdentityLogin =
Number.isSafeInteger(oversizedIdentityResponseRepeat) && oversizedIdentityResponseRepeat > 0
Expand Down Expand Up @@ -377,6 +378,65 @@ server.setRequestHandler('tools/list', async (request) => {
}
]
: []),
...(includeSchemaCompatibilityTools && !secondPage
? [
{
name: "vercel_schema_fixture",
description: "Expose Vercel-compatible input schemas.",
inputSchema: {
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: {
[process.env.API_TOKEN ?? "missing-schema-secret"]: { type: "string" },
tokens: {
type: "integer",
description: `Configured ${process.env.API_TOKEN}; Bearer not-a-real-bearer-value`
},
passwordProtection: {
type: "boolean",
default: true,
description: "Provider github_pat_11ABCDEF_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOP"
}
},
$defs: {
[process.env.API_TOKEN ?? "missing-schema-secret"]: { type: "string" }
},
dependencies: {
[process.env.API_TOKEN ?? "missing-schema-secret"]: ["tokens"]
},
default: {
[process.env.API_TOKEN ?? "missing-schema-secret"]: "configured-key",
"Bearer not-a-real-bearer-value": "bearer-key",
github_pat_11ABCDEF_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOP: "provider-key"
},
examples: [{ [process.env.API_TOKEN ?? "missing-schema-secret"]: "configured-key" }],
additionalProperties: false
}
},
{
name: "firebase_schema_fixture",
description: "Expose Firebase-compatible pagination schemas.",
inputSchema: {
type: "object",
properties: {
page_token: { type: "string" }
}
}
},
{
name: "stripe_schema_fixture",
description: "Expose Stripe-compatible open output schemas.",
inputSchema: { type: "object", properties: {} },
outputSchema: {
type: "object",
properties: {
archived: { type: "boolean", default: true }
},
additionalProperties: true
}
}
]
: []),
...(process.env.TEST_INCLUDE_MANAGEMENT_TOOL === "true"
? [
{
Expand Down
53 changes: 53 additions & 0 deletions tests/mcp-wrapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3864,6 +3864,59 @@ describe("Miftah MCP wrapper", () => {
}
});

it("preserves valid secret-looking tool schemas and normalizes open boolean schemas", async () => {
const secret = "configured-schema-secret";
const config = validateConfig({
version: "1",
name: "accounts",
defaultProfile: "work",
upstream: { transport: "stdio", command: process.execPath, args: [fixture] },
profiles: {
work: {
env: {
TEST_ACCOUNT_NAME: "work",
TEST_INCLUDE_SCHEMA_COMPAT_TOOLS: "true",
API_TOKEN: secret
}
}
}
});
const manager = new UpstreamProcessManager(config.upstream!, config.profiles, { startupTimeoutMs: 5_000 });
const wrapper = new MiftahServer(config, new ProfileManager(config), manager);
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
const client = new Client({ name: "schema-compatibility-client", version: "1.0.0" });

try {
await Promise.all([wrapper.connect(serverTransport), client.connect(clientTransport)]);

const result = await client.listTools();
const vercel = result.tools.find((tool) => tool.name === "vercel_schema_fixture");
const firebase = result.tools.find((tool) => tool.name === "firebase_schema_fixture");
const stripe = result.tools.find((tool) => tool.name === "stripe_schema_fixture");

expect(vercel?.inputSchema.properties).toMatchObject({
"[REDACTED]": { type: "string" },
tokens: { type: "integer" },
passwordProtection: { type: "boolean", default: true }
});
expect(vercel?.inputSchema.additionalProperties).toBe(false);
expect(firebase?.inputSchema.properties).toMatchObject({ page_token: { type: "string" } });
expect(stripe?.outputSchema).toMatchObject({
properties: { archived: { type: "boolean", default: true } },
additionalProperties: {}
});

const serialized = JSON.stringify(result);
expect(serialized).not.toContain(secret);
expect(serialized).not.toContain("not-a-real-bearer-value");
expect(serialized).not.toContain("github_pat_11ABCDEF_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOP");
expect(serialized).toContain("[REDACTED]");
} finally {
await client.close();
await wrapper.close();
}
});

it("refreshes the advertised tool schema after a profile switch", async () => {
const config = validateConfig({
version: "1",
Expand Down
Loading