- Node.js 24+
- pnpm via Corepack:
Corepack reads the
corepack enablepackageManagerfield inpackage.jsonand ensures the correct pnpm version is used automatically.
-
Clone and install dependencies:
git clone https://github.com/davegomez/fizzy-mcp.git cd fizzy-mcp pnpm install -
Verify setup:
pnpm check
You should see
Checked N fileswith no errors. -
Run tests:
pnpm test:run
All 373 tests should pass.
-
Create the tool file
src/tools/yourfeature.ts:import { UserError } from "fastmcp"; import { z } from "zod"; import { getFizzyClient, toUserError } from "../client/index.js"; import { getDefaultAccount } from "../state/session.js"; import { isErr } from "../types/result.js"; function resolveAccount(accountSlug?: string): string { const slug = (accountSlug || getDefaultAccount())?.replace(/^\//, ""); if (!slug) { throw new UserError( "No account specified and no default set. Use fizzy_account first.", ); } return slug; } export const yourFeatureTool = { name: "fizzy_your_feature", description: `One-line summary. **When to use:** - Scenario 1 - Scenario 2 **Arguments:** - \`account_slug\` (optional): Uses session default if omitted - \`required_param\` (required): Description **Returns:** JSON with fields.`, parameters: z.object({ account_slug: z.string().optional(), required_param: z.string(), }), execute: async (args: { account_slug?: string; required_param: string }) => { const slug = resolveAccount(args.account_slug); const client = getFizzyClient(); // Implementation }, };
-
Export from
src/tools/index.ts:export { yourFeatureTool } from "./yourfeature.js";
-
Register in
src/server.ts:import { yourFeatureTool } from "./tools/index.js"; // ... server.addTool(yourFeatureTool);
-
Add tests in
src/tools/yourfeature.test.ts:import { http, HttpResponse } from "msw"; import { describe, expect, it } from "vitest"; import { server } from "../test/mocks/server.js"; import { yourFeatureTool } from "./yourfeature.js"; describe("fizzy_your_feature", () => { it("does expected behavior", async () => { server.use( http.get("https://app.fizzy.do/test-account/endpoint", () => HttpResponse.json({ /* mock */ }) ) ); const result = await yourFeatureTool.execute({ account_slug: "test-account", required_param: "value", }); expect(JSON.parse(result)).toMatchObject({ /* expected */ }); }); });
-
Verify:
pnpm check && pnpm test:run
-
Create a feature branch:
git checkout -b feature/your-feature
-
Make atomic commits using Conventional Commits format:
git commit -m "feat(tools): add fizzy_your_feature tool" -
Run all checks before pushing:
pnpm check && pnpm test:run -
Push and create PR:
git push -u origin feature/your-feature gh pr create
Releases are automated via changelogen and GitHub Actions. The version bump is determined by commit types since the last tag.
-
Preview the release locally (commits and tags locally, does not push):
pnpm release:dry
-
If everything looks good, reset the local changes and run the actual release:
git reset --hard HEAD~1 && git tag -d v<version> pnpm release
This bumps the version in
package.json, updatesCHANGELOG.md, commits, tags, and pushes to the remote. -
The tag push triggers the Release workflow, which runs CI, publishes to npm, and creates a GitHub Release.
| Directory | Purpose |
|---|---|
src/client/ |
HTTP client returning Result<T, FizzyApiError> |
src/schemas/ |
Zod schemas for API types |
src/tools/ |
MCP tool definitions |
src/types/ |
Shared utilities (Result ADT) |
src/state/ |
Session state (default account) |
| File | Purpose |
|---|---|
fizzy.ts |
HTTP client with all API methods. Singleton via getFizzyClient(). |
errors.ts |
Error types (AuthenticationError, NotFoundError, etc.) and toUserError() conversion. |
markdown.ts |
markdownToHtml() and htmlToMarkdown() for content conversion. |
pagination.ts |
encodeCursor() / decodeCursor() for opaque pagination. |
upload.ts |
Direct upload handling for attachments. |
Each domain follows this pattern:
| Schema | Purpose |
|---|---|
EntitySchema |
Full entity shape from API responses |
CreateEntityInputSchema |
Payload for creation endpoints |
UpdateEntityInputSchema |
Partial payload for update endpoints |
{
name: string; // "fizzy_" prefix, snake_case
description: string; // Includes when-to-use, arguments, returns
parameters: ZodObject; // Input validation schema
execute: (args) => Promise<string>; // Returns JSON string
}| Function | Signature |
|---|---|
ok(value) |
<T>(value: T) => Result<T, never> |
err(error) |
<E>(error: E) => Result<never, E> |
isOk(result) |
<T, E>(result: Result<T, E>) => result is Ok<T> |
isErr(result) |
<T, E>(result: Result<T, E>) => result is Err<E> |
| Error | HTTP Status | Description |
|---|---|---|
AuthenticationError |
401 | Invalid or missing token |
ForbiddenError |
403 | Insufficient permissions |
NotFoundError |
404 | Resource not found |
ValidationError |
422 | Invalid request data |
RateLimitError |
429 | Too many requests |
| Command | Purpose |
|---|---|
pnpm dev |
Run with tsx (live reload) |
pnpm build |
Compile TypeScript to dist/ |
pnpm test |
Vitest watch mode |
pnpm test:run |
Run tests once |
pnpm lint |
Check with Biome |
pnpm lint:fix |
Auto-fix lint issues |
pnpm check |
Lint + typecheck (CI-ready) |
pnpm release |
Bump version, update changelog, commit, tag, and push |
pnpm release:dry |
Same as release but without pushing (local preview) |
pnpm test -- src/client/fizzy.test.ts # Single file
pnpm test -- -t "creates a card" # By test name pattern| Rule | Value |
|---|---|
| Formatter | Biome |
| Indentation | Tabs |
| Quotes | Double |
| TypeScript | Strict mode |
| Error handling | Result<T, E> over exceptions |
| Tool naming | fizzy_ prefix, snake_case |