Install
$ agentstack add mcp-ctkadvisors-persona-forge ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
persona-forge
Fine-tune a persona chatbot that never breaks character — and let it search your own books mid-chat with a local RAG "loremaster."
You bring three things: a corpus (plain text you have the rights to use — public-domain classics, your own novel, campaign notes), a base chat model, and any OpenAI-compatible endpoint to act as the data-generation teacher (a local llama.cpp/vLLM/LM Studio server works fine). Everything else is here.
Quickstart
pip install -e ".[data,train]"
# 1. Drop your corpus in:
cp /path/to/your/books.txt data/corpus.txt
# 2. Make your persona pack (start from the Arthurian example):
cp packs/camelot.json packs/mine.json # then edit — see "Customizing" below
# 3. Generate the training blend (points at your teacher endpoint):
PACK=packs/mine.json CORPUS=data/corpus.txt \
OPENAI_BASE_URL=http://localhost:1234/v1 TEACHER=my-teacher-model \
python -m personaforge.build_data
# 4. Train — continued-pretraining on your corpus, then SFT, then DPO,
# as one chained QLoRA adapter on a single GPU:
MODEL_ID=Qwen/Qwen3.6-27B DATA_DIR=out/data DO_CPT=1 CORPUS=data/corpus.txt \
python -m personaforge.train_run
# 5. Prove it holds character (bare assignments, provocations, style probe):
MODEL_ID=Qwen/Qwen3.6-27B ADAPTER=out/adapter PACK=packs/mine.json \
python -m personaforge.battery
# 6. Get numbers (held-out eval -> out/eval.json):
MODEL_ID=Qwen/Qwen3.6-27B ADAPTER=out/adapter PACK=packs/mine.json \
OPENAI_BASE_URL=http://localhost:1234/v1 JUDGE=my-teacher-model \
python -m personaforge.eval_run
The adapter in out/adapter is standard PEFT: merge_and_unload() it and convert to GGUF or MLX for serving with your stack of choice.
Give it a loremaster (RAG over your corpus, via MCP)
Persona models hallucinate lore confidently; retrieval fixes what fine-tuning can't. Build a local index, then register the MCP server in any MCP host (LM Studio, Claude Code, ...) so the model can look passages up mid-chat:
uv run personaforge/rag/build_index.py data/corpus.txt data/index
# in your MCP host's config:
# command: uv
# args: ["run", "/path/to/personaforge/rag/mcp_server.py"]
# env: LORE_INDEX_DIR=/path/to/data/index
# LORE_DESCRIPTION="the collected Arthurian romances"
Models with native tool training keep that ability through the persona tune — they'll pick lore_search for lore questions and narrate the results in voice.
Customizing: the persona pack
All world flavor lives in one JSON file; the pipeline is world-agnostic. Edit these fields in your copy of packs/camelot.json:
| Field | What it does | |---|---| | world | Spliced into every prompt: "in the voice of {world}" | | cards | Your characters: name, persona, style, pronoun | | scenarios | Situation seeds for generated conversations | | provocations | Rude/crude/meta user turns your model must survive in character | | assignments | Casual persona-assignment phrasings ("be {name}", lowercase happens) | | boilerplate | The assistant-speak to train away from (used as DPO rejected) | | exemplars | A few hand-written dialogues that set the voice ceiling |
Knobs on build_data: N_CONVOS / N_DPO / N_ASSIGN / N_ASSIGN_DPO (volumes), IN_DIR (extend an existing blend instead of building fresh). Keep roleplay at 25–35% of the final SFT mix — the generated general-chat blend does this for you at the defaults.
Evals: retrain and swap models with confidence
build_data reserves every 4th provocation/assignment seed — training never sees them. eval_run generates replies to those held-out prompts and scores:
- assignment_accuracy — bare "be {name}" answered by the named character
and no other (string check, free, objective)
- boilerplate_rate — assistant-speak regex over provocation replies (free)
- incharactermean / voice_mean — judge-scored, needs a teacher endpoint
(omit JUDGE to skip; the free metrics still run)
One JSON report per adapter, so comparing a retrain — or a different base model against the same DATA_DIR — is a diff, not a vibe. The battery (personaforge.battery) stays for eyeballing transcripts; the eval is what you gate a release on.
Why the weird data? (the two failure modes)
Naive persona tunes fail two ways, both reproducible: (1) the boilerplate break — the user gets rude or meta and the model drops the persona for "I am a professional and do not engage in such language"; (2) the wrong-character pickup — a bare "hey, you're merlin. roleplay as him" gets answered by a different character introducing themself. Both are data gaps. The pipeline generates conversations that survive provocation, assignments answered by the named character (with a hard wrong-name check), and DPO pairs whose rejected side is the failure verbatim. In our reference run (27B dense, single unified-memory GPU) this took the DPO reward margin from 0.003 to 0.45 and a clean sweep on the battery.
Recipe notes (hard-won)
- Reuse the CPT adapter across retrains: set
CPT_ADAPTER=out/cptand skip
the expensive knowledge stage.
- Pin
device_map={"": 0}on unified-memory boxes;"auto"triggers
CPU-offload chaos. (Already done in the trainers.)
- bitsandbytes 4-bit works on ARM/Blackwell for dense models; MoE models may
crash — set load_in_4bit=False in QLoRAConfig for bf16 LoRA.
- Thinking-mode base models: eval with thinking disabled, and check your chat
template's default before shipping — some inference stacks force-inject enable_thinking=True.
- Expect the judge to keep ~75% of generated conversations and ~90% of
introductions; lower than that usually means a weak teacher model.
License
MIT. No corpora, weights, or copyrighted-world data ship here — the pipeline is yours, your data responsibilities are your own.
Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: ctkadvisors
- Source: ctkadvisors/persona-forge
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.