A client is anything that talks to a Marfa server through an SDK: an app, a CLI, an MCP server, a sync agent, a scheduled job. Clients are not deployment modes — they’re authenticated consumers. This page walks through the pattern using the Marfa MCP server (Documentation Index
Fetch the complete documentation index at: https://docs.myme.so/llms.txt
Use this file to discover all available pages before exploring further.
@withmarfa/mcp) as the worked example. The same pattern applies to CLIs, Raycast extensions, mobile apps, ingest agents, and any other consumer.
Anatomy of a client
Every client has three things:- A credential. One API key (or OAuth grant) with scoped permissions.
- An SDK connection. Typically the TypeScript or Swift SDK, instantiated once and reused.
- An entry point. A CLI command, an MCP tool, an HTTP handler, a scheduled task.
Worked example: @withmarfa/mcp
The Marfa MCP server exposes the Marfa API as MCP tools for Claude and other MCP consumers. A thin TypeScript wrapper around the SDK.
Skeleton
The whole client is thin
The MCP server registers one tool per SDK method. Each tool is a handler that:- Parses input according to a tool-specific schema.
- Calls one SDK method.
- Returns the result as JSON.
Credentials and scope
The MCP server is configured per user via environment variables (MARFA_API_URL, MARFA_API_KEY). Each end-user has their own API key, scoped to their own tenant. The server itself is stateless — starting a new instance with a different key talks to a different tenant.
If you want per-user LLM access with explicit consent, register your client as an OAuth client and use the Authorization Code + PKCE flow instead of static API keys. The MCP server then runs as a public client, exchanges the user’s code for tokens, and refreshes as needed.
LLM-friendly tool descriptions
The MCP server pays close attention to tool descriptions and input schemas because LLMs read them heavily. Drift there is more expensive than in human-facing docs. The same principle applies to any client exposing Marfa capabilities to an LLM:- Use the actual type and edge names (
core.note,in-thread,parent-of) — the LLM has seen them in these docs. - Describe what a field does, not just its type. “
tier: library | feed— curated library vs high-volume feed” is useful; “tier: string” alone isn’t. - Call out enum values explicitly. “
state: active | archived | trashed” beats “state: string”.
Patterns by client kind
Interactive app (iOS, macOS, web)
- Use the SDK in the app’s data layer. Map SDK types into your view models.
- For long-running UIs, subscribe to
/eventsvia the SDK’s SSE iterator. Update the view model when events arrive for items you’re displaying. - Store the API key (or OAuth tokens) in the platform’s secure store (Keychain on Apple, Credential Manager on web).
CLI
- Load the API key from
MARFA_API_KEYor a config file (~/.config/marfa/config.toml). - Provide one command per meaningful workflow:
my items list,my items create,my import. Thin wrappers around SDK calls. - Print results as tables for humans, JSON for machines (controlled by
--jsonor detecting non-TTY stdout).
Ingest agent / sync daemon
- Long-running process with a single API key scoped to the types it writes.
- For pull-based syncs: a scheduled loop calling
GET /items?source=X&updated_after=.... - For push-based: watch the source (a folder, a feed, an inbox) and
POST /itemsas new data appears. Setsource_idon each item — the server resolves(source, source_id)as a natural key, so re-posts upsert rather than duplicate. See Items — natural-key upsert. - Set
default_tier=feedon the credential — ingested items typically land in the high-volume feed slice, not the curated library.
MCP server
- Pattern above. Thin, stateless, one tool per SDK method.
- Run under the LLM host’s process manager (Claude Desktop, Claude Code, Goose).
Integration / webhook consumer
- Register a webhook via
client.webhooks.create. - Accept webhook POSTs, verify the HMAC signature, process asynchronously.
- No need for an SDK on the webhook-receiving side unless you’re calling back into Marfa from the handler.
Optional titles
Several core types carrytitle as an optional property (core.note, core.bookmark, most core.media.*). Apps whose UI exposes a title field write it directly. Apps whose UI doesn’t surface a title — a WYSIWYG notes app where the user types straight into the body, a messages-style capture app — have three consistent patterns:
- Leave null, derive at render. Persist
title: null. Compute a display title from the first line ofbodyat render time. Simplest; follows body edits naturally; nothing to keep in sync. - Derive on save, persist. Compute a title from the body on each save and write it into
title. Other apps see the title directly without needing to know the derivation rule. - Leave blank. Don’t derive. Other apps fall back to body content when they need a display string.
title: null on types where it’s optional — fall back to body (or the equivalent primary text field for the type) for display. The choice of pattern is app-local, not a schema concern.
What doesn’t go in Marfa
The platform is for user-content data — items some other client might meaningfully read. Three categories of data don’t belong on it:- App settings. Theme, default model, UI preferences, layout state. They have no cross-app meaning. Use the OS’s normal mechanism —
UserDefaultson macOS,localStorageon web, iCloud Key-Value Storage if cross-device sync is wanted. - Sync cursors. A sync agent’s per-root cursor is agent-local state. It lives on the agent’s own disk, not on any item in the platform. Same for crawlers, scrapers, or any client tracking external-source position.
- Ephemeral session state. Current scroll position, undo stack, in-flight network state. Stays in the app’s memory.
What clients should not do
- Don’t cache item contents across sessions without a sync mechanism. If your client is long-lived and needs reactive state, use SSE; don’t roll a stale-check poll.
- Don’t re-implement permissions client-side. The server enforces; your job is to surface what the server returned.
- Don’t hide error codes from callers. Propagate
MarfaError(or its localized equivalent) — don’t wrap in generic errors that lose thecode. - Don’t hard-code tenant ids. They’re implicit from the credential; URLs never carry them.
Testing a client
Start a local Marfa server (see Configuration for how to run one). Create a fresh API key, set it asMARFA_API_KEY, and run your client against http://localhost:8600.
The conformance suite is useful as a smoke check: run it against the same server to confirm it’s behaving correctly before running your client against it.