Support multiple Substack publications behind one server - #36
Conversation
Adds an opt-in SUBSTACK_PUB_<KEY>_* env var scheme to configure several publications at once. With exactly one publication configured (the existing SUBSTACK_* vars, or a single prefixed triplet), every tool's schema is unchanged. With two or more, every tool gains a required `publication` enum parameter — no silent default, since two of the tools (create_note, create_note_with_link) publish immediately and irreversibly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
conorbronsdon
left a comment
There was a problem hiding this comment.
Requesting changes at eaa84bb9d99d74bf99e2f03fd8d5b37991e9bb74 for an unsafe configuration fallback in resolvePublications().
Named publication variables whose value is an empty string are skipped by if (!value) continue. If all three variables for a named publication are present but empty, the named group disappears and resolution silently falls back to stored legacy credentials. With one valid named publication plus a second all-empty group, the empty group also disappears, the server enters single-publication mode, and requests can silently route to the valid publication. For immediate Notes publishing, that can irreversibly publish to the wrong account.
Please treat the presence of any named-publication variable as configuration intent, retain the group, and fail validation when its required values are empty or incomplete. Add regression tests for both the all-empty-only case and the valid-A plus all-empty-B case, ensuring neither falls back or collapses to single-publication routing.
I reproduced the stored-credential fallback at the exact head and verified the finding with authenticated Claude Opus 5 and thinkingmachines/inkling:free. The implementation otherwise builds, lints, and its focused publication tests pass.
|
Concrete regression coverage I would add to it("does not fall back when a named publication is present but empty", () => {
const loader = vi.fn(() => stored);
const env = {
SUBSTACK_PUB_SAPERE_PUBLICATION_URL: "",
SUBSTACK_PUB_SAPERE_SESSION_TOKEN: "",
SUBSTACK_PUB_SAPERE_USER_ID: "",
} as NodeJS.ProcessEnv;
expect(() => resolvePublications(env, loader)).toThrow(/sapere/i);
expect(loader).not.toHaveBeenCalled();
});Add the mixed case too: one complete The implementation invariant is that a matching environment-variable name establishes named-publication intent. Create/retain that group before evaluating its value, then let the existing incomplete-group validation reject empty required fields. |
Summary
SUBSTACK_PUB_<KEY>_*env var scheme so one server instance can hold credentials for several publications at once (e.g.SUBSTACK_PUB_KEVIN_MULDOON_PUBLICATION_URL/_SESSION_TOKEN/_USER_ID).SUBSTACK_PUBLICATION_URL/SUBSTACK_SESSION_TOKEN/SUBSTACK_USER_IDset (or a single prefixed triplet), every tool's schema is byte-identical to before — no new parameter appears anywhere.publicationparameter (a Zod enum of the configured keys). No silent default: two tools (create_note,create_note_with_link) publish immediately and irreversibly, so a design that guessed the wrong publication on those calls would be a real, uncorrectable mistake rather than an inconvenience. An unconfigured/missing value is rejected by the MCP SDK's own input validation before the handler — and therefore before any Substack API call — ever runs.src/auth/resolve-publications.tsreuses the existingresolveCredentials()(untouched) for the single-publication fallback path, rather than duplicating its logic.substack-mcp-login,~/.substack-mcp/session.json) is intentionally untouched — it stays single-session; multiple publications are env-var only for now.Test plan
npm run lint— cleannpm run build— cleannpm test— 175/175 passing, including two new suites:resolve-publications.test.ts(env var parsing/fallback/incomplete-triplet/legacy-conflict behavior) andserver-publications.test.ts(schema shape with 1 vs 2+ publications configured, invalid-value rejection before any network call, and correct client routing by key)tools/listshows nopublicationfield anywhere and startup log lines match pre-change wording exactly; with threeSUBSTACK_PUB_<KEY>_*triplets set, confirmedtools/listshowspublicationas required with the right 3-member enum and an unconfigured value is rejected before any network call🤖 Generated with Claude Code