AGENTS.md
+18 −59
https://raw.githubusercontent.com/cyanheads/mailchimp-mcp-server/HEAD/AGENTS.md
− **Version:** 0.3.11− **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.9`− **MCP SDK:** `@modelcontextprotocol/server` `^2.1.0`− ```ts− // src/mcp-server/tools/definitions/mailchimp-send-campaign.tool.ts− import { inputRequired, tool, z } from '@cyanheads/mcp-ts-core';− const ConfirmationSchema = z.object({− confirmed: z.boolean().describe('Confirm campaign dispatch.'),− });− export const mailchimpSendCampaignTool = tool('mailchimp_send_campaign', {− description: 'Compose and send (or schedule/test) a campaign in one call.',− annotations: { destructiveHint: true, openWorldHint: true },− input: InputSchema,− output: OutputSchema,− async handler(input, ctx) {− const svc = getMailchimpService();− let confirmed = input.mode !== 'send';− if (input.mode === 'send') {− const view = ctx.inputs.view('campaignDispatchConfirmation');− if (view.kind === 'missing') {− return ctx.requestInput({− inputRequests: {− campaignDispatchConfirmation: inputRequired.elicit({− message: `Send "${input.subject}" now?`,− requestedSchema: ConfirmationSchema,− }),− },− });− }− confirmed =− view.kind === 'elicit' &&− view.action === 'accept' &&− ctx.inputs.accepted('campaignDispatchConfirmation', ConfirmationSchema)?.confirmed === true;− }− const effectiveMode = confirmed ? input.mode : 'draft';− const draftId = await svc.campaigns.create(ctx, input);− try {− return await svc.campaigns.finalize(ctx, draftId, { ...input, mode: effectiveMode });− } catch (err) {− if (input.cleanupOnError !== false) await svc.campaigns.deleteDraft(ctx, draftId).catch(() => {});+ **Version:** 0.4.0+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.15`+ **MCP SDK:** `@modelcontextprotocol/server` `^2.3.1` (resolved 2.3.1)+ Read `src/mcp-server/tools/definitions/mailchimp-send-campaign.tool.ts` and `mailchimp-replicate-campaign.tool.ts` for the workflow implementations. Both call `confirmCampaignDispatch` before creating a campaign, bind the consent to resolved content with `campaignContentSnapshot`, and reuse checked local HTML afterward. A valid decline leaves a draft; any mismatched or spent record asks again. Cleanup catches only failures after draft creation.+ **HTTP sessions:** `createApp({ sessionMode: { require: 'stateful' } })` keeps campaign confirmation answerable for 2025-era HTTP clients. Explicit `MCP_SESSION_MODE=stateless` refuses HTTP startup; stdio is unaffected. `auto` resolves to stateful. The Mailchimp service's pacer is disposed in `teardown()`.+ **Service errors:** annotate reasons emitted below the handler with `thrownBy: 'service'`. The framework fills a declared recovery hint whenever the failure carries its reason and no explicit hint. `RequestCancelled` is a baseline code and needs no contract entry.+ | `ctx.requestInput` | Always-present control-flow helper for campaign dispatch and new/resumed subscription confirmation before any request writes. The handler returns through it, then restarts on the next round. |+ | `ctx.requestId` / `ctx.traceId` | Framework-generated correlation IDs in the request's log records; errors return `data.requestId`. |+ **Consent:** `ctx.state` holds ten-minute single-use records bound to the operation, caller, target, resolved content, and issuing process. Redeem before mutation; an unknown, changed, expired, replayed, or foreign-instance record asks again. Local redemption excludes concurrent use; another process must re-prompt because storage has no atomic take. Only the random ID crosses rounds; optionally set `MCP_REQUEST_STATE_KEY` to seal it. Restarting invalidates outstanding confirmations.+ **Subscriptions:** `confirmSubscription` shares the consent-record primitive with campaign dispatch. Upsert and primitive update confirm new/resumed `subscribed` status before any writes; already-subscribed first calls omit redundant status. Import confirms once for any valid effective subscribed row without per-row reads. Decline/cancel writes nothing. Reentry must redeem even after upstream status becomes subscribed, so replay cannot repeat notes/tags. Pending/double-opt-in and status-preservi+ **Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()` / `resource()` to get typed `ctx.fail(reason, …)`, auto-populated `data.reason`, and lint-enforced conformance. `recovery` is required (≥5 words): the framework fills it on both client surfaces when a failure carries the reason without a hint. Explicit `{ recovery: { hint: '...' } }` overrides it when runtime context matters. Error envelopes also carry `data.r+ if (!found) throw ctx.fail('audience_not_found', `No audience ${input.id}`);+ | `tool-defs-analysis` | Read-only audit of MCP definition language across the surface — voice, leaks, defaults, recovery hints, output descriptions |+ | `api-context` | Context interface, RequestContext, logger, state, multi-round-trip input |+ | `api-utils` | Formatting, parsing, security, pagination, scheduling, telemetry helpers |+ `bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. After packing, `scripts/clean-mcpb.ts` prunes dev dependencies and strips dependency-shipped agent docs and platform-specific native bindings. MCPB is stdio-only — HTTP deployments are unaffected. Consumers who don't need it can delete `manifest.json` and `.mcpbignore`; `lint:packaging` skips cleanly.+ **Adding an env var requires both files:** `server.json` (registry discovery, `environmentVariables[]`) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match, every `user_config` option is wired into `mcp_config.env` as `"X": "${user_config.X}"`, and optional string options carry `"default": ""`.+ **Every release goes through a release PR, straight-through** — `git-wrapup`'s "Release PR mode", mode `straight-through`. One run: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the release digest: theme line, `## Changes`, `## Gates`, changelog link last); `release-and-publish` then fast-forwards `main` locally with `git merge --ff-only`, creates the tag on `main`'s tip, pushes `main` and the tag, deletes the