brain-fight-mcp
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@brain-fight-mcpstart a debate: should I quit my job?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Brain Fight MCP (v0.1)
A zero-cost MCP server that stages theatrical Angel vs Devil (inner brain fight)
debates. The server never calls an LLM: relationship state, memory, speaker turns, and
optional constraint-axis seeds come from deterministic engines + local SQLite. The
Client LLM performs the dialogue from performance_instructions.
Debate is turn-only:
Step | Tool | Client LLM job |
1 |
| Perform only the returned speaker |
2…n |
| Perform only the next speaker |
end |
| Judge a real |
There is no one-shot full-skit mode. It was removed because a pre-debate winner could quietly corrupt relationship state and recorded outcomes.
Supports stdio (default) and optional Streamable HTTP (--http) for remote MCP
clients, tunnels, or a VPS.
Why
Ask "should I quit my job?" and get Angel arguing caution and Devil arguing boldness — with an evolving relationship (respect, annoyance, cooperation) that changes conflict to conflict, occasional role swaps, domain-scoped track records, and turn-by-turn direction.
Related MCP server: consensus-mcp
Architecture
┌─────────────────────────────────────────────────────────────┐
│ MCP Client (Claude Desktop / Code / other) │
│ • understands the user │
│ • writes constraint-axis seed │
│ • calls tools │
│ • performs Angel 😇 / Devil 😈 from performance_instructions│
└───────────────────────────┬─────────────────────────────────┘
│ MCP (stdio or Streamable HTTP)
┌───────────────────────────▼─────────────────────────────────┐
│ brain-fight-mcp (this process — $0 inference) │
│ tools / resources / prompts │
│ engines: conflict · turn · relationship · continuity · │
│ memory · personality · performance instructions │
│ SQLite: ~/.brain-fight-mcp/state.sqlite3 │
└─────────────────────────────────────────────────────────────┘Responsibility split
Layer | Owns | Does not own |
Server | State, speaker order, seeds/templates, relationship math, memory, director notes | Spoken dialogue, final winner judgment of what was said |
Client LLM | Writing lines in character, judging who argued better, OOC next step | Persisting scores / history (server does that) |
Source layout
src/
index.ts # entry: stdio or --http
server/ # createServer, CLI, HTTP transport
mcp/tools/ # start_debate, continue, end, summon, outcome, …
mcp/resources/ # angel/devil profiles, relationship state
core/ # deterministic engines
db/ # SQLite + repositories
prompts/ # topic templates + prompt helpers
types/ # zod schemas / shared typesEngine pipeline (one debate)
Client drafts a constraint-axis
seed+ picks adomain.start_debate→conflictEnginebuilds stances;turnEnginepicks first speaker andmaxTurns; row lands inactive_conflicts+ firstdebate_turnsrow.Client performs only that speaker from
performance_instructions.continue_conflict_turn(optionallastUtterance/userInterjection/speaker) advances one turn at a time.end_inner_conflictwith Client-judgedwinner→memoryEngine.remember+relationshipEngine.applyConflictResult+ OOC next-step instructions.Later, if the user volunteers what they actually did →
record_decision_outcome.
SQLite tables
Table | Contents |
| Per- |
| Finished conflict history (continuity) |
| Open turn debates (at most one open per session+domain path) |
| Spoken turns for open/closed turn debates |
| Real-world follow-ups the user volunteered |
Character profiles are not in SQLite (static in code).
Default DB path: ~/.brain-fight-mcp/state.sqlite3. Override with BRAIN_FIGHT_DB_PATH.
Retention / forgetting (runs opportunistically on each start_debate via
runStorageMaintenance — no cron):
Zombie open debates —
openrows idle longer than 7 days (BRAIN_FIGHT_OPEN_STALE_DAYS) are markedabandoned, then hard-deleted.Active transcripts — always drop
abandoned; dropcompletedolder than 14 days (BRAIN_FIGHT_RETENTION_DAYS).Durable history cap — keep newest 50
conflictsand 50decision_outcomesper(sessionId, domain)(BRAIN_FIGHT_HISTORY_KEEP/BRAIN_FIGHT_OUTCOME_KEEP). Older rows are deleted (outcomes cascade with their conflict).relationship_statestays one row per bucket (already bounded).VACUUM — optional freelist reclaim after deletes:
default
auto: only if ≥ 25 rows deleted (BRAIN_FIGHT_VACUUM_MIN_DELETED)BRAIN_FIGHT_VACUUM=onalways when anything was deleted;=offnever
Manual full wipe still: reset_relationship (one session/domain) or
clear_database (everything); both need confirm: true.
Continuity recall only reads the recent N finished conflicts (default 3–5),
so rows past the cap (or past the recall window) no longer affect dialogue.
export BRAIN_FIGHT_RETENTION_DAYS=14 # completed active transcript age
export BRAIN_FIGHT_OPEN_STALE_DAYS=7 # idle open → abandoned
export BRAIN_FIGHT_HISTORY_KEEP=50 # max conflicts per session+domain
export BRAIN_FIGHT_OUTCOME_KEEP=50 # max outcomes per session+domain
export BRAIN_FIGHT_VACUUM=auto # auto | on | offInstall & Run
Requirements: Node.js ≥ 20. First install may compile native module better-sqlite3
(needs Xcode Command Line Tools on macOS, or build tools on Linux/Windows).
From npm
npx -y brain-fight-mcpFrom source
git clone https://github.com/rkny6/brain-fight-mcp.git
cd brain-fight-mcp
npm install
npm run build
npm startAdd to an MCP client (Claude Desktop / Claude Code / etc.)
{
"mcpServers": {
"brain-fight": {
"command": "npx",
"args": ["-y", "brain-fight-mcp"]
}
}
}Local development:
{
"mcpServers": {
"brain-fight": {
"command": "node",
"args": ["/absolute/path/to/brain-fight-mcp/dist/index.js"]
}
}
}Streamable HTTP (remote / tunnel / VPS)
Default transport is still stdio. For Streamable HTTP:
npm run build
npm run start:http
# or:
# node dist/index.js --http --port 8000 --token 'replace-me'
export BRAIN_FIGHT_HTTP_TOKEN='replace-me'
export BRAIN_FIGHT_HTTP_PORT=8000
node dist/index.js --httpPath | Purpose |
| Liveness + auth flag |
| Streamable HTTP MCP endpoint |
Auth (recommended before any public URL):
Header:
Authorization: Bearer <token>Or query:
?token=<token>
http://127.0.0.1:8000/mcp
# client header: Authorization: Bearer replace-meQuick tunnel:
# terminal 1
node dist/index.js --http --host 127.0.0.1 --port 8000 --token 'replace-me'
# terminal 2
cloudflared tunnel --url http://127.0.0.1:8000
# remote client URL: https://<random>.trycloudflare.com/mcp (+ same Bearer token)Notes:
HTTP MCP session (
Mcp-Session-Id) is transport-level and separate from toolsessionId(SQLite continuity).Binding
--host 0.0.0.0without a token is warned; use a token for public exposure.This process still does not call an LLM. A remote MCP client with an LLM must connect and perform the debate.
Use a stable sessionId across rounds for continuity. Different projects/users should
use different session IDs so state does not collide.
Remote / mobile clients (self-host)
Anyone can git clone / npx this server and run it themselves. Whether a phone app
can use it depends on that app, not on this repo.
Setup | Works? | Typical clients |
Local stdio on a computer | ✅ | Claude Desktop, Claude Code, Cursor, other desktop MCP hosts that spawn a local process |
Self-hosted Streamable HTTP on a VPS / home machine + tunnel | ✅ if the client supports remote/custom HTTP MCP | Desktop or mobile apps that let you paste an MCP URL + auth |
Run Node on the phone as a local stdio MCP | ❌ practically no | Mobile apps almost never spawn a local Node MCP process |
Important: this server is only the director/scoreboard. The client still needs an LLM (Claude, etc.) to perform Angel/Devil lines. Server inference cost stays $0; client usage is billed by whoever provides the model.
Recommended path for remote / phone-capable apps
Deploy HTTP somewhere reachable (VPS, or laptop + Cloudflare Tunnel).
Always set a strong
--token(orBRAIN_FIGHT_HTTP_TOKEN) before any public URL.Prefer HTTPS via reverse proxy or tunnel; do not expose bare HTTP on the open internet without TLS if you can avoid it.
In the client, add a remote MCP / custom connector pointing at:
https://<your-host>/mcp
Authorization: Bearer <your-token>Confirm the app speaks Streamable HTTP MCP (same family as this server's
/mcpendpoint). If it only supports stdio, local desktop, or a closed connector catalog, it will not attach to your self-hosted URL.Keep using a stable tool
sessionIdin debates so relationship/memory continuity survives across chats; that is independent of the HTTPMcp-Session-Id.
Minimal VPS-style example
git clone https://github.com/rkny6/brain-fight-mcp.git
cd brain-fight-mcp
npm install
npm run build
# bind only as needed; put a reverse proxy / tunnel in front for TLS
export BRAIN_FIGHT_HTTP_TOKEN='long-random-secret'
node dist/index.js --http --host 127.0.0.1 --port 8000 --token "$BRAIN_FIGHT_HTTP_TOKEN"Then either:
point nginx/Caddy at
127.0.0.1:8000with HTTPS, orrun
cloudflared tunnel --url http://127.0.0.1:8000and use the issuedhttps://….trycloudflare.com/mcpURL + Bearer token in the client.
Smoke-check:
curl -sS https://<your-host>/health
# expect JSON with ok: true (and an auth flag when token mode is on)What mobile Claude (and similar apps) can / cannot do
Can (sometimes): connect to your self-hosted HTTP MCP if that app's settings expose remote/custom MCP connectors and accept Streamable HTTP + Bearer auth.
Cannot (usually): install this package on the phone and run it as a local stdio server the way Claude Desktop does on a Mac/PC.
Always true: dialogue quality still depends on the client model; this process only returns state +
performance_instructions.
App support changes over time — check your client's docs for “remote MCP”, “custom MCP server”, or “HTTP connector”. This README documents the server side; it cannot guarantee every mobile Claude build will expose a hook for arbitrary self-hosted MCPs.
Usage guide (Client LLM)
This server is a director / scoreboard, not a dual-agent system. One Client LLM still plays both voices when asked.
Recommended client workflow
Understand the user — restate the dilemma in their concrete details.
Write a constraint-axis
seedfirst — rails, not finished monologue lines:tension,angelMust,devilMust, optionaluserDetails,forbidden.Pick a
domain—career|money|relationships|health|general. Trust and outcome history are bucketed per(sessionId, domain). Don't default togeneralwhen a clearer bucket applies.Call
start_debatewithseed+domain.Perform only the returned speaker from
performance_instructions— do not paste raw JSON or recite tension/must rails as spoken lines.continue_conflict_turnwithlastUtterance/ optionaluserInterjectionuntilshouldEnd/ max turns / user stops.end_inner_conflictwith your own judgedwinner(who actually argued better). The silent default is a pre-debatelikelyWinner, not a verdict on the dialogue.If the user later volunteers what they did / how it went →
record_decision_outcome. Never ask them to file a report.
Seed vs no-seed
seed = director rails for this debate, not finished monologue lines.
The server turns rails into performance_instructions; the Client LLM invents the spoken dialogue.
Who | Role |
User | States a real dilemma in their own words |
Client LLM | Writes |
This server | Never writes dialogue; converts seed/templates into director notes + state |
Preferred constraint-axis seed:
Field | Meaning | Not |
| The real opposing axis for this round | Not an opening monologue |
| Argument obligation Angel must hold | Not Angel's script |
| Argument obligation Devil must hold | Not Devil's script |
| Concrete nails that must appear (people / money / deadlines) | Not a summary paragraph |
| Empty slogans / tropes to ban | Not style adjectives |
Resolution order inside start_debate:
Has
seed→ use it (constraint axes preferred; legacy full-stance seed still accepted).No
seed→ score EN/ZH topic templates oncontext/topic:keyword hit (longer phrases win), else
token-overlap near-miss against template rails, else
GENERIC_TOPIC(Safety vs Freedom).
Always (no seed) → auto-extract
userDetailsanchors from free-form context (money, times, quoted phrases, common life nouns, content tokens) into the debate brief / CONSTRAINT AXES, plus anti-reciteforbiddenrails.
With a seed, keyword/overlap templates are skipped entirely (seed overrides templates).
Legacy seeds with empty userDetails also get a context backfill.
Constraint seed (recommended) | No seed, keyword/overlap | No seed → generic | |
Where the axis comes from | Client, from the user's real facts | Best-scored canned topic template | Fixed Safety vs Freedom |
Dialogue specificity | Highest (Client-written | Medium–high (template + auto | Low axis, but still grounded by extracted facts |
Recite risk | Lower (rails ≠ finished lines) | Medium (canned positions + anti-recite bans) | Higher |
Best for | Real product path | Demos / classic / near-miss dilemmas | Last-resort when nothing matches |
Server cost | Still $0 | $0 | $0 |
No seed? Allowed — the server will not error. Quality is better than a bare
Safety-vs-Freedom dump, but still below a real constraint seed:
Matching is still deterministic heuristics (keyword length + token overlap), not an LLM understanding of the story — messy multi-axis dilemmas can still miss.
Generic still collapses weak-signal text onto Safety-vs-Freedom, but extracted
userDetailsforce concrete nouns into the performance rails.Template stances remain fuller canned positions (easier to recite than constraint rails);
forbidden+ anti-recite rules push the Client LLM off pure copy-paste.domainonly buckets relationship / track-record scores; it does not invent a better debate axis.Continuity callbacks do not rewrite the core tension; for unique cases still write
seed/userDetailsyourself.
Mental model:
Main path = Client writes seed → specific, grounded fight.
Fallback = no seed → best template + auto anchors; runnable and less empty than before; still weaker for one-of-a-kind situations.
Turn-by-turn flow
Client writes constraint-axis seed + domain
start_debate(seed=..., domain=...)
→ Client performs ONLY speaker A
continue_conflict_turn(lastUtterance=..., userInterjection?=...)
→ Client performs ONLY speaker B
... until turn.shouldEnd / maxTurns / user stops ...
end_inner_conflict(winner=...) # Client-judged
→ relationship settles + OOC next-step instructions
# optional, days later, only if user volunteered:
record_decision_outcome(conflictId=..., actualChoice=..., sentiment?=...)Open:
{
"name": "start_debate",
"arguments": {
"context": "Should I quit my job tomorrow?",
"intensity": "medium",
"domain": "career",
"firstSpeaker": "devil",
"sessionId": "my-project",
"seed": {
"tension": "Quit tomorrow vs secure a runway first.",
"angelMust": "Rent runway; one dramatic day does not pay next month.",
"devilMust": "Heat of the decision is now; delay is rehearsing fear.",
"userDetails": ["quit tomorrow", "next month's rent", "pointless standup"],
"forbidden": ["generic safety vs freedom slogan"]
}
}
}Continue:
{
"name": "continue_conflict_turn",
"arguments": {
"sessionId": "my-project",
"lastUtterance": "Devil: Send the email before you talk yourself out of it.",
"userInterjection": "But rent is due next week…",
"speaker": "angel"
}
}End:
{
"name": "end_inner_conflict",
"arguments": {
"sessionId": "my-project",
"winner": "angel",
"lastUtterance": "Angel: Pad the runway first."
}
}Record a real-world follow-up (only what the user volunteered):
{
"name": "record_decision_outcome",
"arguments": {
"sessionId": "my-project",
"conflictId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"actualChoice": "angel",
"outcomeNote": "I waited two weeks and got a written offer.",
"sentiment": "good"
}
}Turn rules
First speaker priority:
firstSpeaker→ opposite ofrecentWinner→ highdevilAnnoyance→ highangelRespect+ lowcooperation→ topic bias → default DevilMid debate: alternate;
speakerforce wins; rare double-tap (~12%) when tensemaxTurnsby intensity: low=2, medium=4, high=6Relationship / finished memory update only on
end_inner_conflictStarting a new debate abandons any previous open debate in that session/domain path
Prefer low intensity for trivial decisions (fewer turns) instead of inventing a one-shot mode
Solo takes
{ "name": "summon_angel", "arguments": { "context": "Should I text my ex?", "sessionId": "my-project" } }{ "name": "summon_devil", "arguments": { "context": "Should I text my ex?", "sessionId": "my-project" } }These do not open a conflict or change relationship scores. Client LLM writes a
first-person monologue from performance_instructions.
Tools
Tool | Purpose |
| Open a turn-by-turn debate (one speaker per call). |
| Next speaker after |
| Close debate: memory + relationship + OOC next step. Pass Client-judged |
| Angel's solo perspective (no conflict). |
| Devil's solo perspective (no conflict). |
| Record what the user later said they actually did / how it went. |
| Relationship scores + recent conflicts + track record (domain-aware). |
| Wipe one session (optionally one domain). Requires |
| Wipe all sessions / tables. Requires |
Most tools accept optional sessionId (default "default"). clear_database is global.
Tool parameters
start_debate
Arg | Required | Default | Description |
| yes | — | Dilemma / situation text |
| no | — | Short topic label |
| no |
|
|
| no |
| State isolation key |
| no |
|
|
| no | auto |
|
| no | — | Write this first (Client LLM). Prefer constraint axes |
Preferred seed (constraint axes — invent dialogue from rails, do not recite):
Field | Required | Description |
| yes | What this round is about in one line |
| yes | Argument obligation / axis for Angel |
| yes | Argument obligation / axis for Devil |
| no | Concrete facts (deadlines, people, money, systems) |
| no | Optional bans (e.g. generic Safety-vs-Freedom slogan) |
Legacy full-stance seed fields are still accepted but easier to accidentally recite.
continue_conflict_turn
Arg | Required | Default | Description |
| no |
| Session key |
| no | open conflict | Active debate id from start |
| no | auto | Force |
| no | — | User's latest remark to answer |
| no | — | Previous performed line (stored + used for reaction) |
end_inner_conflict
Arg | Required | Default | Description |
| no |
| Session key |
| no | open conflict | Active debate id |
| no* | engine |
|
| no | — | Final performed line to store |
*Omitting winner falls back to a pre-debate random/weighted draw. Treat that as last resort.
record_decision_outcome
Arg | Required | Default | Description |
| yes | — |
|
| yes | — |
|
| no |
| Session key |
| no | — | Brief note in the user's words |
| no | — |
|
summon_angel / summon_devil
Arg | Required | Default | Description |
| yes | — | Situation text |
| no | — | Short label |
| no |
| Session key |
get_relationship
Arg | Required | Default | Description |
| no |
| Session key |
| no |
| Pin one domain, or omit for general + |
reset_relationship / clear_database
Tool | Scope | Required arg |
| One |
|
| Entire SQLite file |
|
{ "name": "reset_relationship", "arguments": { "sessionId": "my-project", "confirm": true } }{ "name": "clear_database", "arguments": { "confirm": true } }Resources
angel://profile— static Angel personality profiledevil://profile— static Devil personality profilerelationship://state— live relationship state for the default sessionrelationship://state/{sessionId}— live relationship state for a specific session
Prompts
Prompt | Purpose |
| Draft |
| Call |
| Call |
How the engine works
Server side (deterministic, $0 inference):
Session + domain isolation — continuity is keyed by
(sessionId, domain);reset_relationshipclears one session (optionally one domain);clear_databaseempties the whole DB. Both requireconfirm: true.Topic matching / seeds — Client
seedoverrides templates. Otherwise score keyword + token-overlap topic templates (expanded EN/ZH sets), then generic Safety-vs-Freedom when weak; always extractuserDetailsfrom context on the no-seed path. Recommended: always writeseedfirst. Output is plain seed stances / rails, not the final script.Intensity scaling —
low/medium/high→ extremity 0.3 / 0.6 / 0.9, pacing rules inperformance_instructions, andmaxTurns2 / 4 / 6.Relationship bias — high
angelRespect→ Angel more accommodating; highdevilAnnoyance→ Devil more contrarian.Role reversal — if
cooperation > 0.7, ~15% chance Angel/Devil swap archetypes (isRoleReversal: true).Continuity —
recallprior finished conflicts for the same session/domain and inject callbacks into seeds / instructions.Turn director — first/next speaker, double-taps; delays finished memory + relationship settlement until
end_inner_conflict.Outcomes — optional real-world follow-ups feed
get_relationshiptrack records and future continuity (only when the user volunteered them).
Client side:
Script generation — Client LLM writes dialogue from cast, beats, intensity, continuity, and constraint axes. Do not recite seed lines verbatim.
Winner judgment — Client decides who argued better when ending; then one out-of-character actionable next step.
Relationship update rules
Applied only when a conflict is settled via end_inner_conflict:
Angel wins →
angelRespect += 0.02,devilAnnoyance += 0.03Devil wins →
devilRespect += 0.02,angelAnnoyance += 0.03Draw →
cooperation += 0.05Role reversal occurred →
cooperation += 0.02(stacks)
All values clamped to [0, 1].
Development
npm run dev # stdio with tsx, no build step
npm run dev:http # Streamable HTTP with tsx on :8000
npm run typecheck # tsc --noEmit
npm test # vitest run
npm run build # tsup -> dist/index.jsSee CONTRIBUTING.md for PR guidelines.
Publish (maintainers)
npm login
npm publish --access publicprepublishOnly runs typecheck, tests, and build. Package contents are limited to
dist/, LICENSE, and README.md (see "files" in package.json).
Design notes
Zero server LLM cost — engines + SQLite only; no API keys on this process.
Plan A script generation — server is state + constraints; Client LLM generates dialogue.
Turn mode is director protocol, not dual agents — one Client LLM still performs both voices; the server picks speaker, tracks turns, and settles relationship on end.
Domain bucketing — career trust and snack-decision trust do not share one score.
Client-judged winners — pre-debate
likelyWinneris only a fallback.stdio by default; optional HTTP for remote MCP clients (use
--token).Comedy over accuracy: Devil isn't always wrong, Angel isn't always right.
License
MIT © rkny6
Brain Fight MCP (v0.1) 中文
零成本 MCP 服务:编排戏剧化的 天使 vs 恶魔(内心 brain fight)辩论。
服务端从不调用 LLM:关系状态、记忆、发言顺序、可选的约束轴 seed,全部由确定性
引擎 + 本地 SQLite 完成。客户端 LLM 根据 performance_instructions 表演对白。
辩论是 纯回合制(turn-only):
步骤 | 工具 | 客户端 LLM 要做的事 |
1 |
| 只表演返回的那一位发言者 |
2…n |
| 只表演下一位 |
结束 |
| 自行判定真实 |
没有一次性完整小品(full-skit)模式。已移除:因为辩论前抽签的 winner 会悄悄污染 关系分和后续 outcome 记录。
默认 stdio;可选 Streamable HTTP(--http)给远程客户端 / 隧道 / VPS。
为什么做这个
问「我该不该辞职?」——天使讲谨慎,恶魔讲大胆;关系(尊重、恼火、合作)会跨场次演化, 偶尔角色互换,按生活领域分桶记战绩,并按回合导演发言。
架构
┌─────────────────────────────────────────────────────────────┐
│ MCP 客户端(Claude Desktop / Code / 其他) │
│ • 理解用户处境 │
│ • 先写约束轴 seed │
│ • 调用工具 │
│ • 按 performance_instructions 表演天使 😇 / 恶魔 😈 │
└───────────────────────────┬─────────────────────────────────┘
│ MCP(stdio 或 Streamable HTTP)
┌───────────────────────────▼─────────────────────────────────┐
│ brain-fight-mcp(本进程 — 推理费用 $0) │
│ tools / resources / prompts │
│ 引擎:conflict · turn · relationship · continuity · │
│ memory · personality · performance instructions │
│ SQLite:~/.brain-fight-mcp/state.sqlite3 │
└─────────────────────────────────────────────────────────────┘职责划分
层 | 负责 | 不负责 |
服务端 | 状态、发言顺序、seed/模板、关系分、记忆、导演备注 | 对白正文、对「谁说得更好」的最终裁决 |
客户端 LLM | 入戏写台词、判定谁更有说服力、戏外下一步 | 持久化分数/历史(服务端做) |
源码结构
src/
index.ts # 入口:stdio 或 --http
server/ # createServer、CLI、HTTP 传输
mcp/tools/ # start_debate、continue、end、summon、outcome…
mcp/resources/ # 天使/恶魔人设、关系状态
core/ # 确定性引擎
db/ # SQLite + repositories
prompts/ # 话题模板与 prompt 辅助
types/ # zod schema / 共享类型一场辩论的流水线
客户端起草约束轴
seed,并选择domain。start_debate→conflictEngine生成立场;turnEngine选首位发言者与maxTurns;写入active_conflicts与首条debate_turns。客户端只表演该发言者。
continue_conflict_turn(可选lastUtterance/userInterjection/speaker) 一次推进一回合。end_inner_conflict带上客户端判定的winner→ 记记忆 + 更新关系 + 戏外下一步。若用户事后主动说了真实选择/结果 →
record_decision_outcome。
SQLite 表
表 | 内容 |
| 按 |
| 已结束冲突历史(连续性) |
| 进行中的回合辩论 |
| 各回合发言记录 |
| 用户主动反馈的真实世界结果 |
角色人设不进 SQLite(代码内静态)。
默认库路径:~/.brain-fight-mcp/state.sqlite3。可用 BRAIN_FIGHT_DB_PATH 覆盖。
保留 / 遗忘(每次 start_debate 顺带跑 runStorageMaintenance,无 cron):
僵尸 open — 闲置超过 7 天(
BRAIN_FIGHT_OPEN_STALE_DAYS)的open先标abandoned,再硬删。进行中/已完成 transcript — 始终删
abandoned;删超过 14 天的completed(BRAIN_FIGHT_RETENTION_DAYS)。持久历史上限 — 每个
(sessionId, domain)只保留最新 50 条conflicts与 50 条decision_outcomes(BRAIN_FIGHT_HISTORY_KEEP/BRAIN_FIGHT_OUTCOME_KEEP);更老的删除 (outcome 随 conflict 级联)。relationship_state每桶一行,本身有界。VACUUM — 删除后可选回收空闲页:
默认
auto:一次删 ≥ 25 行才 VACUUM(BRAIN_FIGHT_VACUUM_MIN_DELETED)BRAIN_FIGHT_VACUUM=on有删就做;=off永不
手动全清仍用:reset_relationship(单 session/domain)或 clear_database(全库),
均需 confirm: true。
连续性 recall 只读最近 N 条已结束冲突(默认 3–5),超出 cap / 窗口的不再影响对白。
export BRAIN_FIGHT_RETENTION_DAYS=14
export BRAIN_FIGHT_OPEN_STALE_DAYS=7
export BRAIN_FIGHT_HISTORY_KEEP=50
export BRAIN_FIGHT_OUTCOME_KEEP=50
export BRAIN_FIGHT_VACUUM=auto安装与运行
要求: Node.js ≥ 20。首次安装可能编译原生模块 better-sqlite3
(macOS 需 Xcode Command Line Tools;Linux/Windows 需对应构建工具)。
用 npm
npx -y brain-fight-mcp从源码
git clone https://github.com/rkny6/brain-fight-mcp.git
cd brain-fight-mcp
npm install
npm run build
npm start接入 MCP 客户端
{
"mcpServers": {
"brain-fight": {
"command": "npx",
"args": ["-y", "brain-fight-mcp"]
}
}
}本地开发:
{
"mcpServers": {
"brain-fight": {
"command": "node",
"args": ["/absolute/path/to/brain-fight-mcp/dist/index.js"]
}
}
}Streamable HTTP(远程 / 隧道 / VPS)
npm run build
npm run start:http
# 或:
# node dist/index.js --http --port 8000 --token 'replace-me'
export BRAIN_FIGHT_HTTP_TOKEN='replace-me'
export BRAIN_FIGHT_HTTP_PORT=8000
node dist/index.js --http路径 | 用途 |
| 探活 + 是否启用鉴权 |
| Streamable HTTP MCP 端点 |
鉴权(公网前建议开启):
Header:
Authorization: Bearer <token>或 query:
?token=<token>
说明:
HTTP 的
Mcp-Session-Id是传输层会话,与工具参数sessionId(SQLite 连续性)无关。--host 0.0.0.0且无 token 会有警告;公网务必加 token。本进程仍不调用 LLM;需要带 LLM 的 MCP 客户端来表演辩论。
跨场次请使用稳定的 sessionId。不同项目/用户用不同 session,避免状态串台。
远程 / 手机客户端(自建部署)
任何人都可以 git clone / npx 后自己跑这个服务。手机 App 能不能接上,取决于该
App 是否支持远程/自定义 MCP,不取决于本仓库有没有 HTTP 模式。
部署方式 | 能不能用 | 典型客户端 |
电脑本地 stdio | ✅ | Claude Desktop、Claude Code、Cursor 等会拉起本地进程的桌面 MCP 宿主 |
自建 Streamable HTTP(VPS / 家里机器 + 隧道) | ✅ 前提是客户端支持远程/自定义 HTTP MCP | 允许填写 MCP URL + 鉴权的桌面或手机 App |
在手机里跑 Node 当本地 stdio MCP | ❌ 基本不行 | 手机 App 几乎不会在本机 spawn Node MCP 进程 |
重要: 本服务只做导演/记分板。客户端仍需要带 LLM(Claude 等)来演天使/恶魔 台词。服务端推理费用 $0;模型用量按客户端提供方计费。
远程 / 可能支持手机的推荐路径
把 HTTP 服务部署到可访问的地方(VPS,或笔记本 + Cloudflare Tunnel)。
任何公网 URL 前都设置强随机
--token(或BRAIN_FIGHT_HTTP_TOKEN)。尽量用反向代理或隧道提供 HTTPS;能避免就不要把裸 HTTP 直接暴露在公网。
在客户端里添加 远程 MCP / 自定义 connector,指向:
https://<你的域名或隧道>/mcp
Authorization: Bearer <你的 token>确认该 App 使用的是 Streamable HTTP MCP(与本服务
/mcp同类)。若它只支持 stdio、仅桌面本地、或封闭的官方 connector 列表,就接不上你的自建 URL。辩论时继续使用稳定的工具参数
sessionId,关系/记忆连续性才能跨对话保留;这与 HTTP 的Mcp-Session-Id无关。
最小 VPS 示例
git clone https://github.com/rkny6/brain-fight-mcp.git
cd brain-fight-mcp
npm install
npm run build
# 按需绑定;前面再挂反向代理 / 隧道做 TLS
export BRAIN_FIGHT_HTTP_TOKEN='long-random-secret'
node dist/index.js --http --host 127.0.0.1 --port 8000 --token "$BRAIN_FIGHT_HTTP_TOKEN"然后任选:
用 nginx/Caddy 反代
127.0.0.1:8000并上 HTTPS,或跑
cloudflared tunnel --url http://127.0.0.1:8000,在客户端填https://….trycloudflare.com/mcp+ Bearer token。
探活:
curl -sS https://<你的主机>/health
# 期望返回带 ok: true 的 JSON(开启 token 时还会带鉴权相关字段)手机 Claude(及类似 App)能做什么 / 不能做什么
有时可以: 若该 App 设置里开放了远程/自定义 MCP,并接受 Streamable HTTP + Bearer 鉴权,就可以连你的自建服务。
通常不行: 像桌面 Claude Desktop 那样在手机上安装本包、用本地 stdio 拉起进程。
始终成立: 对白质量仍取决于客户端模型;本进程只返回状态 +
performance_instructions。
各 App 能力会变——请查其文档里的 “remote MCP”、“自定义 MCP”、“HTTP connector” 等 说明。本 README 只保证服务端怎么部署;不能保证每一个手机 Claude 版本都提供挂载 任意自建 MCP 的入口。
使用指南(客户端 LLM)
服务端是导演 / 记分板,不是双 Agent 系统。同一个客户端 LLM 仍可扮演两边。
推荐工作流
理解用户 — 用具体细节复述困境。
先写约束轴
seed— 是轨道不是成稿台词:tension、angelMust、devilMust, 可选userDetails、forbidden。选
domain—career|money|relationships|health|general。 信任与战绩按(sessionId, domain)分桶;能分清就不要偷懒用general。调用
start_debate,带上seed+domain。只表演返回的发言者;不要粘贴原始 JSON,也不要把 tension/must 当台词念。
continue_conflict_turn,传lastUtterance/ 可选userInterjection,直到shouldEnd/ 达到 max turns / 用户叫停。end_inner_conflict传入你自己判定的winner(谁在这场里更有说服力)。 省略时的默认是辩论前的likelyWinner,不是对台词的裁决。用户事后主动说了真实选择/结果 →
record_decision_outcome。不要主动逼用户填表。
Seed 有无对比
seed = 本场辩论的导演轨道/约束,不是成稿独白。
服务端把轨道收成 performance_instructions;真正说话的是客户端 LLM。
角色 | 做什么 |
用户 | 用自己的话讲真实纠结 |
客户端 LLM | 先写 |
本服务 | 不写对白;把 seed/模板变成导演备注 + 状态 |
推荐的约束轴 seed:
字段 | 含义 | 不是什么 |
| 本场真正对立的那一根轴 | 不是开场白 |
| 天使这轮必须守住的论证义务 | 不是天使台词 |
| 恶魔这轮必须守住的论证义务 | 不是恶魔台词 |
| 必须写进对白的具体钉子(人/钱/期限) | 不是摘要段落 |
| 禁止滑回去的空话/套路 | 不是风格形容词 |
start_debate 内解析顺序:
有
seed→ 直接用(优先约束轴;旧版全立场 seed 仍接受)。没有
seed→ 对context/topic给中英话题模板打分:关键词命中(更长短语优先),否则
token 重叠近邻匹配模板轨道,否则
GENERIC_TOPIC(安全 vs 自由)。
无 seed 时始终 → 从自由文本自动抽取
userDetails锚点(金额、时间、引号片段、 常见生活名词、内容词)写入 brief / CONSTRAINT AXES,并附带反照念forbidden。
有 seed 时,关键词/重叠模板整段跳过(seed 覆盖模板)。
旧版全立场 seed 若没带 userDetails,也会从 context 回填。
约束轴 seed(推荐) | 无 seed,关键词/重叠 | 无 seed → generic | |
争论轴从哪来 | 客户端根据用户实情写 | 得分最高的预制话题模板 | 固定「安全 vs 自由」 |
对白具体程度 | 最高(客户端写的 | 中高(模板 + 自动 | 轴偏空,但仍用抽取事实接地 |
照念风险 | 较低(轴不是成稿) | 中等(成品立场 + 反照念禁令) | 较高 |
适合 | 正式产品路径 | 演示 / 经典 / 近邻题 | 弱信号时的保底 |
服务端成本 | 仍 $0 | $0 | $0 |
没有 seed? 可以——服务端不会报错。比「直接掉进安全 vs 自由」好,但仍弱于真 seed:
匹配仍是确定性启发式(关键词长度 + token 重叠),不是 LLM 理解故事;多轴纠结仍可能装错。
弱信号仍会落到「安全 vs 自由」,但自动
userDetails会把具体名词推进表演轨道。模板立场更接近成品句子(比约束轴更容易照念);
forbidden+ 反照念规则逼客户端现场写。domain只分桶关系分/战绩,不会生成更贴题的争论轴。连续性回调不会重写本场核心张力;独特处境仍应自己写
seed/userDetails。
心智模型:
主路径 = 客户端写 seed → 吵到点上、贴用户事实。
Fallback = 没 seed → 最佳模板 + 自动锚点;能跑且比以前不那么空;一对一独特题仍偏弱。
回合流程
客户端写 seed + domain
start_debate(seed=..., domain=...)
→ 只表演发言者 A
continue_conflict_turn(lastUtterance=..., userInterjection?=...)
→ 只表演发言者 B
... 直到 shouldEnd / maxTurns / 用户停止 ...
end_inner_conflict(winner=...) # 客户端判定
→ 结算关系 + 戏外下一步
# 可选,数天后,仅当用户主动提起:
record_decision_outcome(conflictId=..., actualChoice=..., sentiment?=...)开场示例:
{
"name": "start_debate",
"arguments": {
"context": "我明天要不要裸辞?",
"intensity": "medium",
"domain": "career",
"firstSpeaker": "devil",
"sessionId": "my-project",
"seed": {
"tension": "明天就辞 vs 先把跑道垫好。",
"angelMust": "下月房租;戏剧性的一天付不了账单。",
"devilMust": "热度就在现在;再拖只是排练恐惧。",
"userDetails": ["明天辞职", "下月房租", "毫无意义的站会"],
"forbidden": ["空泛的安全 vs 自由口号"]
}
}
}继续:
{
"name": "continue_conflict_turn",
"arguments": {
"sessionId": "my-project",
"lastUtterance": "恶魔:在你把自己说服回去之前先把邮件发出去。",
"userInterjection": "可是下周房租就到期了…",
"speaker": "angel"
}
}结束:
{
"name": "end_inner_conflict",
"arguments": {
"sessionId": "my-project",
"winner": "angel",
"lastUtterance": "天使:先把跑道垫好。"
}
}记录真实世界跟进(只记用户主动说的):
{
"name": "record_decision_outcome",
"arguments": {
"sessionId": "my-project",
"conflictId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"actualChoice": "angel",
"outcomeNote": "我等了两周,拿到了书面 offer。",
"sentiment": "good"
}
}回合规则
首位发言优先级:
firstSpeaker→ 与recentWinner相反 → 高devilAnnoyance→ 高angelRespect+ 低cooperation→ 话题偏向 → 默认恶魔中途: 交替;强制
speaker优先;关系紧张时约 12% 同侧连击(double-tap)maxTurns按 intensity: low=2,medium=4,high=6关系分 / 完结记忆只在
end_inner_conflict更新新开辩论会放弃该 session/domain 路径上未结束的旧辩论
琐碎决定用 low intensity(更少回合),不要幻想 one-shot 模式
单人视角
{ "name": "summon_angel", "arguments": { "context": "我该不该给前任发消息?", "sessionId": "my-project" } }{ "name": "summon_devil", "arguments": { "context": "我该不该给前任发消息?", "sessionId": "my-project" } }不会开冲突、不改关系分。客户端按 performance_instructions 写第一人称独白。
工具
工具 | 用途 |
| 开启回合制辩论(每次只返回一位发言者) |
|
|
| 结束:记忆 + 关系 + 戏外下一步;传入客户端判定的 |
| 天使单人视角(无冲突) |
| 恶魔单人视角(无冲突) |
| 记录用户事后主动说的真实选择/结果 |
| 关系分 + 近期冲突 + 战绩(支持 domain) |
| 清空一个 session(可选单个 domain)。需 |
| 清空全部 session / 表。需 |
多数工具可选 sessionId(默认 "default")。clear_database 是全局的。
参数摘要
start_debate
参数 | 必填 | 默认 | 说明 |
| 是 | — | 困境 / 处境 |
| 否 | — | 短标签 |
| 否 |
|
|
| 否 |
| 状态隔离键 |
| 否 |
|
|
| 否 | 自动 |
|
| 否 | — | 建议先写;优先约束轴 |
推荐 seed(约束轴 — 据此发明台词,不要照念):
字段 | 必填 | 说明 |
| 是 | 本场争什么(一句话) |
| 是 | 天使必须守住的论证轴 |
| 是 | 恶魔必须守住的论证轴 |
| 否 | 具体事实(期限、人、钱、制度) |
| 否 | 禁区(如空泛口号) |
continue_conflict_turn
参数 | 必填 | 默认 | 说明 |
| 否 |
| session |
| 否 | 当前 open | 开场返回的 id |
| 否 | 自动 | 强制 |
| 否 | — | 用户插话 |
| 否 | — | 上一句已表演台词(入库 + 供反应) |
end_inner_conflict
参数 | 必填 | 默认 | 说明 |
| 否 |
| session |
| 否 | 当前 open | 辩论 id |
| 否* | 引擎 |
|
| 否 | — | 最后一句入库 |
*省略 winner 会退回辩论前的加权/随机结果,只应作最后手段。
record_decision_outcome
参数 | 必填 | 默认 | 说明 |
| 是 | — | start/end 返回的 id |
| 是 | — |
|
| 否 |
| session |
| 否 | — | 用户原话式短注 |
| 否 | — |
|
get_relationship
参数 | 必填 | 默认 | 说明 |
| 否 |
| session |
| 否 |
| 指定领域;省略则 general + |
危险操作
工具 | 范围 | 必填 |
| 一个 |
|
| 整个 SQLite 文件 |
|
资源
angel://profile— 静态天使人设devil://profile— 静态恶魔人设relationship://state— 默认 session 的实时关系relationship://state/{sessionId}— 指定 session 的实时关系
Prompts
Prompt | 用途 |
| 写 seed → |
| 调 |
| 调 |
引擎如何工作
服务端(确定性,$0 推理):
Session + domain 隔离 — 连续性按
(sessionId, domain);reset_relationship/clear_database均需confirm: true。话题匹配 / seed — 客户端 seed 覆盖模板;否则按关键词 + token 重叠给扩展中英 话题模板打分,弱信号再退回「安全 vs 自由」;无 seed 路径始终从 context 抽取
userDetails。建议总是先写 seed。输出是立场种子,不是最终剧本。Intensity — low/medium/high → 激烈度 0.3/0.6/0.9,以及
maxTurns2/4/6。关系偏置 — 高
angelRespect天使更迁就;高devilAnnoyance恶魔更抬杠。角色互换 —
cooperation > 0.7时约 15% 互换原型(isRoleReversal: true)。连续性 —
recall同 session/domain 的旧冲突,注入回调。回合导演 — 首位/下一位、连击;完结记忆与关系结算推迟到
end_inner_conflict。Outcome — 用户主动反馈的真实结果进入战绩与后续连续性。
客户端:
写剧本 — 根据人设、关系节拍、intensity、连续性、约束轴写对白;禁止照念 seed。
判定胜者 — 结束时由客户端判断谁更有说服力,再给一句戏外可执行下一步。
关系分更新规则
仅在 end_inner_conflict 结算时应用:
天使胜 →
angelRespect += 0.02,devilAnnoyance += 0.03恶魔胜 →
devilRespect += 0.02,angelAnnoyance += 0.03平局 →
cooperation += 0.05发生角色互换 →
cooperation += 0.02(可叠加)
全部夹紧到 [0, 1]。
开发
npm run dev # tsx stdio,无需先 build
npm run dev:http # tsx Streamable HTTP :8000
npm run typecheck # tsc --noEmit
npm test # vitest run
npm run build # tsup -> dist/index.jsPR 指南见 CONTRIBUTING.md。
发布(维护者)
npm login
npm publish --access publicprepublishOnly 会跑 typecheck、测试与 build。包内容仅含 dist/、LICENSE、README.md。
设计备注
服务端零 LLM 成本 — 只有引擎 + SQLite;本进程无 API key。
Plan A 剧本生成 — 服务端管状态与约束;客户端 LLM 生成对白。
回合制是导演协议,不是双 Agent — 同一客户端仍演两边;服务端选人、计回合、结束时结算。
Domain 分桶 — 职业信任与零食决定不共用一个分数。
客户端判定胜者 — 辩论前的
likelyWinner只是兜底。默认 stdio;远程用 HTTP 时请加
--token。喜剧优先于「永远正确」:恶魔不总是错,天使不总是对。
许可证
MIT © rkny6
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- Alicense-qualityDmaintenanceA Model Context Protocol server that enables collaborative debates between multiple AI agents, allowing them to discuss and reach consensus on user prompts.Last updated1MIT
- Alicense-qualityAmaintenanceAn MCP server that enables multi-model debate and consensus building through a single tool. It orchestrates multiple AI models from various providers to debate topics and reach validated conclusions with real-time progress tracking.Last updated793MIT
- Alicense-qualityDmaintenanceMCP server for AI agents to conduct multi-LLM roundtable discussions, returning structured common, divergent, and unique perspectives.Last updatedMIT
- Alicense-qualityCmaintenanceMCP server that reduces confirmation bias in LLMs by orchestrating structured debates between asymmetric context sessions.Last updatedMIT
Related MCP Connectors
MCP server for AI dialogue using various LLM models via AceDataCloud
Person-owned, portable AI memory as a remote MCP server, readable and writable by any MCP client.
Cloud-hosted MCP server for durable AI memory
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/rkny6/brain-fight-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server