Brain API Reference
The Brain API is a public developer endpoint that exposes your MarketingSecrets.ai memory to any MCP-compatible client (Claude Desktop, Cursor, ChatGPT, or your own scripts). Authenticate with a per-user API key for read/write access to your entrepreneur profile, memory facts, and semantic memory.
Base URL & protocol
POST https://www.marketingsecrets.ai/api/mcp
JSON-RPC 2.0 over HTTP POST (Model Context Protocol). All requests must use HTTPS.
Authentication
Send your API key in the x-api-key header:
x-api-key: ms_YOUR_KEY
- Keys use the
ms_prefix + 64 hex characters (67 total). - Generate keys in Chief of Staff → Manage → Memories → Connect an AI you already use (Advanced tab). The plaintext is shown once and is never kept server-side — it's stored as a SHA-256 hash. Revoke anytime.
- Every method needs a key, including the handshake.
initialize,notifications/initialized, andpingall return-32001 Authentication requiredwithout one, so send your key on the very first request.
Tools
| Tool | Purpose |
|---|---|
memory_search | Keyword search across your Brain — every word of your query must appear somewhere in a fact, in any order. Args: query (required), limit (default 8). |
memory_get_profile | Returns your structured entrepreneur profile: business, niche, audience, focus. No args. |
memory_save | Saves a fact, indexed for both semantic search and structured retrieval. Args: content (required), category (default misc). |
memory_get_recent | Lists the most recent structured facts in chronological order. Args: limit (default 20). |
Categories for memory_save: business, audience, preferences, projects, imported, misc.
Inventory tools
These are read-only and paginated. They exist so a client can enumerate your Attractive Character material rather than having to already know a uuid.
| Tool | Purpose |
|---|---|
list_attractive_characters | Your Attractive Characters — id, name, tagline, character type, polarity, active flag. Args: query, limit, offset. |
get_attractive_character | One full character sheet: backstory, origin story, core flaw, pillars, epiphany moments, voice style, plus story and voice counts. Args: id (required). |
list_voices | The voice style sheets attached to your characters, with each sheet's length and first few hundred characters. Args: ac_profile_id, query, limit, offset. |
get_voice | One voice style sheet in full. Args: id (required). |
list_stories | A page of your story inventory, newest first — id, title, summary, themes, emotion. Args: ac_profile_id, limit, offset. |
search_stories | The same page, filtered. query matches title, summary, body, false belief, emotion and lesson as a substring, and themes and custom tags as a whole tag. Args: query (required), ac_profile_id, limit, offset. |
get_story | One story in full: body, hooks, tags, false belief and every narrative-arc field. Args: id (required). |
list_hooks | Your hook library — every candidate hook on your stories, one per row, each with the story_id it came from. Args: ac_profile_id, limit, offset. |
search_hooks | The same, filtered to hooks containing query. Args: query (required), ac_profile_id, limit, offset. |
list_customer_avatars | Your customer avatars, each as a full sheet — age range, platform, occupation, top fear, top desire, demographics, psychographics. Args: query, limit, offset. |
get_customer_avatar | One customer avatar sheet. Args: id (required). |
Paging. Every list tool takes limit (default 25, maximum 50 — a larger value is clamped, not rejected) and offset (default 0). The response is a JSON object with an items array and a page object:
{
"ok": true,
"kind": "stories",
"items": [{ "id": "…", "title": "…", "summary": "…", "themes": [], "emotion": "hope" }],
"page": {
"limit": 25,
"offset": 0,
"returned": 25,
"total": 873,
"has_more": true,
"next_offset": 25
},
"hint": "Showing 25 of 873 stories (offset 0). More exist — call again with offset: 25."
}
Page by copying page.next_offset into the next call's offset, and stop when page.has_more is false. page.total is the real number of rows matching your call — never treat one page as the whole library.
Three limits are worth knowing:
- Story embeddings are never returned.
list_storiesandsearch_storiesreturnid,ac_profile_id,title,summary,themesandemotiononly; callget_storyfor the body. - A page can be shortened to fit the response. If the rows on a page would exceed the response size budget, the tail is dropped,
truncated_for_sizeis set totrue, andpage.returned/page.next_offsetare adjusted so followingnext_offsetstill skips nothing. - Hook paging walks stories. Hooks live inside their story rows, so
list_hooks/search_hooksread a bounded window of stories per call and report it asscan. Page within the window withoffset; whenscan.cappedistruethe walk stopped short of your whole inventory, so continue withstory_offset: <scan.next_story_offset>andoffset: 0. Never conclude a hook does not exist from a single capped page.
Every inventory tool only ever sees the workspace that minted your key. An ac_profile_id argument is resolved against that workspace's own characters, so it accepts a character's name as well as its uuid.
Attractive Character write tools
These are the only correct way for a client to record an Attractive Character. Saving a persona with memory_save puts it in your Brain as a note and leaves your Attractive Characters page empty — that is not the same thing, and the write tools exist so a client never has to settle for it.
| Tool | Purpose |
|---|---|
create_attractive_character | Creates a new character, ready to use immediately. Args: name (required), tagline, character_type, core_flaw, polarity, backstory, origin_story, pillars, voice_style. |
update_attractive_character | Changes only the fields you pass. Args: ac_profile_id (required), base_revision (required), plus any field create_attractive_character takes. |
create_ac_voice | Creates a voice style sheet for one content type, ready to use immediately. Args: ac_profile_id, name, content_type (all required), speaker_role, style_notes, sample_passages, calibration_source. |
update_ac_voice | Edits an existing voice. style_sheet is a full replacement. Args: voice_id (required), base_revision (required), name, content_type, speaker_role, style_sheet, target_speaker_name. |
save_attractive_character_story | Adds one story to your story inventory. Args: ac_profile_id, title (both required), summary, body, themes, emotion, false_belief, candidate_hooks, timestamp_start, timestamp_end, and the ten narrative acts (backstory, desires, external_conflict, internal_conflict, the_wall, epiphany, the_plan, the_conflict, the_achievement, the_transformation). |
Four rules govern all five, and a client that ignores them gets a refusal rather than a surprise:
- What a client creates is yours to use straight away. A new character or voice is live the moment it is saved: you can open it, generate from it, add voices to it, and a routine may use it. There is no draft state and nothing to confirm — a client that tells you to go and approve the character is wrong.
- Every update needs
base_revision. Read the record first (list_attractive_characters,get_attractive_character,list_voices,get_voice, or therevisiona previous create/update returned) and send thatrevisionvalue back asbase_revision. If the record changed in between — most often because you edited it yourself in the app — nothing is written, and the refusal carries the livecurrent_revisionto merge against. A call without abase_revisionis refused the same way. This is what stops a client from quietly overwriting your own edits. - There is no delete. Removing a character, a voice or a story is an in-app action only.
- Apology text is not a story.
save_attractive_character_storyrejects placeholder content ("I couldn't find any stories…") instead of filing it, so a client that found nothing has to tell you rather than leave a phantom row behind.
A refused write comes back as a normal tools/call result with isError set to true and a JSON body explaining what to fix — not as a JSON-RPC error — because every one of these refusals is recoverable within the same conversation. The one exception is a connection with no workspace in scope, which fails with -32001.
A successful create or update returns the new revision, so a follow-up edit in the same conversation needs no extra read, plus a view_url like /home/<workspace>/attractive-characters/<id> to open what was just written.
Customer avatar write tool
Your customer avatars — the dream buyers you write to, as opposed to the character you write as — have their own create tool, for the same reason. Saving a persona with memory_save files it in your Brain as a note and leaves your Customer Avatars page empty.
| Tool | Purpose |
|---|---|
create_customer_avatar | Creates a new customer avatar. Args: name (required), age_range, platform, occupation, demographics, psychographics, top_fear, top_desire, color. |
Three things govern it:
- Several avatars are normal. Unlike an Attractive Character, a workspace is expected to have more than one dream buyer, so a second create is not treated as a mistake. What is caught is an accidental repeat: creating the same name twice within ten minutes, or creating a name that is saved with no profile yet (what the Create button on the Customer Avatars page makes), fills in the empty fields of the avatar that already exists, flagged
already_created: true, instead of saving a duplicate. Values already saved are never overwritten. - There is no update and no delete. Editing or removing an avatar is an in-app action only, on your Customer Avatars page. A client that wants to change one has to tell you rather than write a second copy.
- Only
nameis required. Everything else is optional on purpose: a thin avatar built from what you actually said beats a padded one, anddemographics/psychographicsare free-form objects so a client can record whatever it genuinely learned.
A successful create returns the new avatar_id, the full avatar row, and a view_url of /home/<workspace>/customer-avatars.
Every tool returns its payload as text on the JSON-RPC envelope at result.content[0].text. The sections below give the exact shape of that text per tool.
memory_search
Keyword search over your business brain. It covers the facts you saved, facts a teammate shared into the workspace, and the Team Brain.
Your query is split into words, and a fact matches when every word appears somewhere in its key or value, in any order — so stock watchlist finds a fact reading "current stock watchlist (Q3)". Matches come back newest-first.
When nothing matches every word, the search widens on its own, in order:
- Any word instead of all of them, ranked by how many words each fact hits.
- Translated and synonym keywords. A question asked in one language still finds a fact saved in another — asking
Aký je môj cieľreaches a fact stored as "September 2026 revenue target".
Short, distinctive queries are still the best input — one to four words like a name, a topic, or a metric. Fall back to memory_get_recent when a search comes up empty.
Arguments: query (string, required), limit (number, default 8)
Response lines are numbered and carry the category, plus the fact key when one exists:
1. [audience] ideal_customer: bootstrapped agencies doing $500K-$2M/yr
2. [business] pricing: $997 coaching, 4 slots per cohort
memory_get_profile
Returns the structured entrepreneur profile: business name, niche, audience, social handles, current focus.
Arguments: none ({})
Response text is one Label: value pair per line:
Business: Acme Agency
Niche: B2B SaaS marketing
Audience: bootstrapped SaaS founders doing $200K-$2M ARR
Current focus: launching a cold-email SOP
If nothing has been captured yet you get No entrepreneur profile found yet., and if the profile exists but is empty, Profile exists but is empty.
memory_save
Save a new fact to the brain. Indexed for semantic search and stored as a structured fact.
Arguments: content (string, required), category (string, default misc)
Returns a confirmation naming the category it filed under, e.g. Saved to brain (audience).
memory_get_recent
List the most recent structured facts in chronological order — no semantic ranking.
Arguments: limit (number, default 20)
Response lines are [category] key: value, unnumbered:
[audience] ideal_customer: bootstrapped agencies doing $500K-$2M/yr
[projects] q3_launch: cold-email SOP ships Sept 12
Example: search your Brain
curl -X POST https://www.marketingsecrets.ai/api/mcp \
-H "x-api-key: ms_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "memory_search",
"arguments": { "query": "who is my ideal customer?", "limit": 5 }
}
}'
The same call in TypeScript:
const res = await fetch('https://www.marketingsecrets.ai/api/mcp', {
method: 'POST',
headers: {
'x-api-key': process.env.MS_BRAIN_KEY!,
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'tools/call',
params: {
name: 'memory_search',
arguments: { query: 'who is my ideal customer?', limit: 5 },
},
}),
});
const { result } = await res.json();
console.log(result.content[0].text);
Responses follow the JSON-RPC envelope — the text payload lives at result.content[0].text:
{
"jsonrpc": "2.0",
"id": 1,
"result": { "content": [{ "type": "text", "text": "…" }] }
}
Example: save a fact
curl -X POST https://www.marketingsecrets.ai/api/mcp \
-H "x-api-key: ms_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "memory_save",
"arguments": {
"content": "Ideal customer: bootstrapped agencies doing $500K-$2M/yr.",
"category": "audience"
}
}
}'
Error codes
Errors come back as JSON-RPC error objects. Check error.message for a human-readable explanation.
| Code | Name | Meaning |
|---|---|---|
-32700 | PARSE_ERROR | Body is not valid JSON |
-32600 | INVALID_REQUEST | Missing jsonrpc or method |
-32601 | METHOD_NOT_FOUND | Unknown method or tool |
-32602 | INVALID_PARAMS | Missing required argument |
-32603 | INTERNAL_ERROR | Server-side failure |
-32001 | UNAUTHORIZED | Missing or invalid x-api-key |
-32002 | PAYMENT_REQUIRED | The workspace's subscription is no longer active |
All but -32001 and -32002 are standard JSON-RPC 2.0 codes; those two are our own, in the implementation-defined server range.
-32002 is returned on tools/call only (HTTP 402) — initialize and tools/list keep working, so a client can still connect and see why. The error object's data carries action_url and action_label pointing at the page where the plan can be reactivated.
For developers building sub-apps
Sub-apps inside the MarketingSecrets ecosystem can read from and write to the shared Brain instead of building their own memory layer. A Brand Voice sub-app that learns the user's Attractive Character should write it to the Brain so every other tool — CoS chat, content calendar, cold-email agent — has it automatically.
See the Developer Guide for the full pattern.
Next
- Connect an AI You Already Use — setup for Claude Desktop, Cursor, ChatGPT
- MCP usage examples — real prompts and tool calls
- Developer Guide — build sub-apps on top of the Brain