Universal MCP server for Ragmir — local-first RAG knowledge base. Manage projects, upload files, ingest, and search all via MCP over HTTP.
Remote AI agents (OpenCode, Claude, Cursor, etc.) can create projects, provide files, build vector indexes, and query the knowledge base — without SSH or CLI access.
- 23 MCP tools — project management, file operations, indexing, search (single-project + cross-project), knowledge accumulation, and hot-reload admin
- Remote access — any agent on the LAN can use it via HTTP
- Multi-project — unlimited projects, each with its own vector index
- OpenAPI docs — auto-generated interactive docs at
/docs - Zero cloud — fully local, no data leaves your network
[Remote Agent / OpenCode] [Open WebUI]
│ │
│ SSE (MCP transport) │ REST (OpenAPI)
├──────────────┐ ▼
▼ ▼ mcpo :8000
mcp-proxy :8001 mcp-proxy :8003 │
(ragmir MCP) (ragmir-upload MCP) │
│ │ │ stdio
│ stdio │ stdio ▼
▼ ▼ ragmir-server.js
ragmir-server.js upload-client.mjs │
│ │ │
│ │ HTTP POST │
│ ▼ │
│ upload-server :8002 │
│ │ │
├──────────────┴──────────────────────┤
│ │
├── rgr CLI (search, ingest, ask, research)
└── /opt/ragmir-projects/ (project storage)
Ports:
- Port 8001 — SSE transport (MCP protocol) for
ragmirtools, OpenCode / Claude / Cursor - Port 8003 — SSE transport (MCP protocol) for
ragmir-uploadtools, exposesupload_to_ragmirto remote agents - Port 8000 — REST/OpenAPI proxy (via mcpo) for Open WebUI
- Port 8002 — raw HTTP
/uploadendpoint (used byragmir-uploadSSE above and by clients that prefer curl/scripts)
Services:
ragmir-mcp(mcpo) — port 8000, REST/OpenAPIragmir-sse(mcp-proxy) — port 8001, SSE/MCP forragmirragmir-upload-mcp(mcp-proxy) — port 8003, SSE/MCP wrapper forupload-client.mjsragmir-upload(upload-server) — port 8002, raw HTTP upload endpoint
- Node.js >= 22
- npm (comes with Node)
- uv or pip (for mcpo)
curl -sSL https://raw.githubusercontent.com/cioinside/ragmir-mcp-server/main/install.sh | bash# 1. Install Ragmir CLI
npm install -g @jcode.labs/ragmir
# 2. Install server
sudo mkdir -p /usr/local/lib/ragmir-server
sudo cp server.js /usr/local/lib/ragmir-server/
sudo chmod +x /usr/local/lib/ragmir-server/server.js
# 3. Create directories
sudo mkdir -p /opt/ragmir-projects
sudo mkdir -p /etc/ragmir
# 4. Generate mcpo config
sudo tee /etc/ragmir/mcpo-config.json << 'EOF'
{
"mcpServers": {
"ragmir": {
"command": "node",
"args": ["/usr/local/lib/ragmir-server/server.js"],
"env": {
"RAGMIR_PROJECTS_DIR": "/opt/ragmir-projects"
}
}
}
}
EOF
# 5. Install systemd service (edit API key first!)
sudo cp ragmir-mcp.service /etc/systemd/system/
sudo sed -i 's/CHANGE-ME/YOUR_SECRET_KEY/' /etc/systemd/system/ragmir-mcp.service
sudo systemctl daemon-reload
sudo systemctl enable --now ragmir-mcp
# 6. Open firewall
sudo ufw allow 8000/tcp| Variable | Default | Description |
|---|---|---|
RAGMIR_MCP_INSTALL_DIR |
/usr/local/lib/ragmir-server |
Server installation path |
RAGMIR_PROJECTS_DIR |
/opt/ragmir-projects |
Project storage directory |
RAGMIR_MCP_PORT |
8000 |
REST/OpenAPI port (mcpo) |
RAGMIR_MCP_API_KEY |
CHANGE-ME |
API key for authentication |
RAGMIR_SSE_PORT |
8001 |
SSE port for ragmir MCP |
RAGMIR_UPLOAD_PORT |
8002 |
Raw HTTP /upload port |
RAGMIR_UPLOAD_MCP_PORT |
8003 |
SSE port for ragmir-upload MCP |
RAGMIR_UPLOAD_MCP_HOST |
127.0.0.1 |
Bind host for upload-mcp SSE (set 0.0.0.0 to expose on LAN) |
# Check service status
systemctl status ragmir-mcp
# Check tools
curl -s http://localhost:8000/ragmir/openapi.json | python3 -m json.toolcurl -s -X POST http://localhost:8000/ragmir/ragmir_create_project \
-H "Authorization: Bearer CHANGE-ME" \
-H "Content-Type: application/json" \
-d '{"name":"my-api","description":"REST API backend"}'curl -s -X POST http://localhost:8000/ragmir/ragmir_write_files_batch \
-H "Authorization: Bearer CHANGE-ME" \
-H "Content-Type: application/json" \
-d '{
"project":"my-api",
"files":[
{"path":"README.md","content":"# My API\n\nREST API service."},
{"path":"src/auth.py","content":"def authenticate(email, password):\n return check(email, password)"},
{"path":"docs/api.md","content":"# API Docs\n\n## POST /auth\nAuthenticate user."}
]
}'# Add sources
curl -s -X POST http://localhost:8000/ragmir/ragmir_add_sources \
-H "Authorization: Bearer CHANGE-ME" \
-H "Content-Type: application/json" \
-d '{"project":"my-api","patterns":["docs/**/*.md","src/**/*.py","README.md"]}'
# Ingest
curl -s -X POST http://localhost:8000/ragmir/ragmir_ingest \
-H "Authorization: Bearer CHANGE-ME" \
-H "Content-Type: application/json" \
-d '{"project":"my-api"}'curl -s -X POST http://localhost:8000/ragmir/ragmir_search \
-H "Authorization: Bearer CHANGE-ME" \
-H "Content-Type: application/json" \
-d '{"project":"my-api","query":"How does authentication work?","topK":3}'import requests
SERVER = "http://192.168.1.100:8000"
HEADERS = {"Authorization": "Bearer CHANGE-ME", "Content-Type": "application/json"}
# Create project
r = requests.post(f"{SERVER}/ragmir/ragmir_create_project", headers=HEADERS,
json={"name": "my-api", "description": "REST API"})
# Upload files
r = requests.post(f"{SERVER}/ragmir/ragmir_write_files_batch", headers=HEADERS,
json={"project": "my-api", "files": [
{"path": "README.md", "content": "# My API"},
{"path": "src/main.py", "content": "def main(): pass"},
]})
# Ingest
r = requests.post(f"{SERVER}/ragmir/ragmir_ingest", headers=HEADERS,
json={"project": "my-api"})
# Search
r = requests.post(f"{SERVER}/ragmir/ragmir_search", headers=HEADERS,
json={"project": "my-api", "query": "main function"})
print(r.json())| Tool | Description |
|---|---|
ragmir_create_project |
Create a new project (directory + rgr init) |
ragmir_delete_project |
Delete project and all data |
ragmir_list_projects |
List all projects |
ragmir_project_status |
Project status (files, chunks, config) |
| Tool | Description |
|---|---|
ragmir_write_file |
Write one file to a project |
ragmir_write_files_batch |
Write multiple files at once |
ragmir_read_file |
Read a file from a project |
ragmir_list_files |
List files in a project |
ragmir_delete_file |
Delete a file |
| Tool | Description |
|---|---|
ragmir_add_sources |
Add glob patterns for indexing |
ragmir_ingest |
Run ingestion (after adding files) |
ragmir_search |
Search one project with citations |
ragmir_search_all |
Universal search across all projects — call this when you don't know which project holds the answer (prevents creating duplicate corpora) |
ragmir_ask |
Get context for a question (no LLM) |
ragmir_research |
Multi-query research |
| Tool | Description |
|---|---|
ragmir_append_file |
Append text to an existing file (auto-backup + auto-ingest) |
ragmir_edit_file |
Find/replace within a file (auto-backup + auto-ingest) |
ragmir_supersede_note |
Mark old record as superseded, create new one (preserves history) |
ragmir_list_history |
List all backup versions of a file |
ragmir_diff_versions |
Show diff between two file versions |
ragmir_restore_version |
Restore a file from a specific backup |
ragmir_health_check |
Quick health summary (fast status or deep audit) |
ragmir_delete_file |
Delete a file (now includes autoIngest to clean orphaned chunks) |
ragmir_admin_reload_tools |
Notify all clients to re-fetch tools/list (MCP notifications/tools/list_changed). Also fires on SIGHUP to the server process. |
For agents that want to accumulate and update knowledge records over time:
Each knowledge record is a single small file (2-10 KB). Records can be YAML, JSON, or Markdown. This keeps the system simple, editable by any agent, and easy to version-control.
experience/
task-001-auth-implementation/
note.yaml # Primary record
note-v2.yaml # Superseded by note.yaml (via supersede)
task-002-payment/
note.md
| Action | Tool |
|---|---|
| Create a new record | ragmir_write_file |
| Add findings to existing record | ragmir_append_file (with timestamp separator) |
| Update a specific field | ragmir_edit_file (find/replace) |
| Found a better method? | ragmir_supersede_note — preserves old + links to new |
- Every write/edit/append/delete auto-backs up to
.ragmir-history/BEFORE the operation - Every mutation auto-triggers
rgr ingest(incremental — only the changed file is re-embedded) - Use
ragmir_list_history+ragmir_diff_versionsto review before destructive ops - Use
ragmir_restore_versionto roll back if needed
# 1. Create initial record
curl -s -X POST http://localhost:8000/ragmir/ragmir_write_file \
-d '{"project":"my-kb","path":"experience/task-001/note.yaml","content":"# Auth\nmethod: JWT\n"}'
# 2. Append a finding
curl -s -X POST http://localhost:8000/ragmir/ragmir_append_file \
-d '{"project":"my-kb","path":"experience/task-001/note.yaml","content":"## Finding\nAdd rate limiting."}'
# 3. Found a better method — supersede
curl -s -X POST http://localhost:8000/ragmir/ragmir_supersede_note \
-d '{"project":"my-kb","oldPath":"experience/task-001/note.yaml","newPath":"experience/task-001/note-v2.yaml","newContent":"# Auth\nmethod: OAuth2\n","reason":"OAuth2 more secure than JWT"}'
# 4. Verify health
curl -s -X POST http://localhost:8000/ragmir/ragmir_health_check \
-d '{"project":"my-kb","deep":true}'All backups go to .ragmir-history/<relative-path>/<ISO-timestamp>.bak inside the project directory. The .ragmir-history directory is hidden (dot-prefixed) so it's excluded from the ragmir index.
- List backups:
ragmir_list_history(project="my-kb", path="experience/task-001/note.yaml") - Diff versions:
ragmir_diff_versions(project="my-kb", path="experience/task-001/note.yaml", versionA="2026-08-03T10-50-53-000Z.bak", versionB="current") - Restore:
ragmir_restore_version(project="my-kb", path="experience/task-001/note.yaml", version="2026-08-03T10-50-53-000Z.bak")
When multiple agents share a ragmir server, the collective memory is only as useful as the discipline that maintains it. The full protocol lives in MASIP.md — read it before contributing. This section is the executive summary.
ragmir_search_all("<task-id> <question>", topK=3, totalLimit=10) # default — covers all projects
# OR, if you already know the target project:
ragmir_search(project, "<task-id> <question>", topK=5)
If the answer exists in ragmir, don't write a new record. If you find a better answer elsewhere, supersede (don't overwrite). ragmir_search_all is the recommended first step because it queries every project in one call and tags each hit with its source — preventing the failure mode where a new agent creates a duplicate corpus because they didn't know prior work existed in another project.
| Situation | Right tool |
|---|---|
| New finding in an existing record | ragmir_append_file (timestamp separator) |
| Field-level edit | ragmir_edit_file (find/replace) |
| Found a better approach entirely | ragmir_supersede_note — old record stays queryable with status: superseded + superseded_by: <new path> |
| New type of problem solved | ragmir_write_file with YAML frontmatter (metadata is indexed, sidecar files are not) |
| Identified wrong/outdated entry | ragmir_edit_file to set status: incorrect, never ragmir_delete_file (orphans chunks) |
Every knowledge record SHOULD include:
---
project_context: "task-001-auth-rotation" # domain identifier
environment: [prod, ci_cd]
tech_stack: [nodejs, express, jwt]
quality_score: 0-10 # self-assessed
version: "1.0"
supersedes: "" # path of older record, if v2+
agent_id: "your-agent-id"
timestamp: "2026-08-03T12:30:00Z"
---Frontmatter is part of the indexed file body — ragmir_search will find it. Sidecar note.meta.yaml files are not indexed.
- One project per knowledge domain. Don't mix
python-fastapiandkubernetes-deployments— search quality degrades. - Abstract patterns go to a dedicated project (e.g.
patterns/orexperience-records). Includesource_project: <name>in the frontmatter so provenance is queryable. - Conflicting best practices: keep both, distinguish via
context_scopein frontmatter. Never delete the loser.
ragmir_health_check(project="my-kb", deep=true)
Must report staleInIndex=0, missingFromIndex=0, duplicateCandidates=0. If not, do not paper over it.
See MASIP.md for the full protocol with examples, anti-patterns, and tool mappings.
Text files (.py, .md, .js) → use ragmir_write_files_batch MCP tool.
Binary files (.docx, .pdf, .xlsx, images) → two options:
Option A — MCP tool (recommended for remote agents): connect via ragmir-upload SSE on port :8003, the agent gets an upload_to_ragmir tool. See Connecting from Remote OpenCode.
Option B — raw HTTP upload endpoint (too large for MCP tool calls):
# Linux/Mac
curl -X POST http://192.168.1.100:8002/upload \
-F "project=my-project" \
-F "path=docs/report.docx" \
-F "file=@/path/to/report.docx"
# Windows PowerShell
Invoke-WebRequest -Uri "http://192.168.1.100:8002/upload" `
-Method POST `
-Form @{project="my-project"; path="docs/report.docx"; file=Get-Content "C:\path\report.docx" -Encoding Byte}Response:
{"ok":true,"project":"my-project","path":"docs/report.docx","bytes":12345,"ingested":true}Files auto-ingest on upload. If autoIngest=false, call ragmir_ingest via MCP afterwards.
Add to ~/.config/opencode/opencode.jsonc on the remote PC:
Replace 192.168.1.100 with your server's IP.
Port 8001 serves the SSE transport for ragmir tools. Port 8003 serves the SSE transport for ragmir-upload (binary file upload via the upload_to_ragmir tool). Port 8000 is REST/OpenAPI (for Open WebUI). Port 8002 is the raw HTTP /upload endpoint.
Both ragmir and ragmir-upload can run as remote MCPs from any machine on the LAN — no local file paths needed, no /root access required.
For agents on Windows that need to upload binary files (.docx, .pdf, .xlsx, images) without writing code, use the upload-client local MCP server. It provides an upload_to_ragmir tool that reads files from the local disk and sends them to the remote server.
- Clone the repo and install dependencies:
git clone https://github.com/cioinside/ragmir-mcp-server.git C:\ragmir-mcp-server
cd C:\ragmir-mcp-server\upload-client
npm install- Add to
opencode.jsonc(oropencode.jsonin the project root):
{
"mcp": {
"ragmir": {
"type": "remote",
"url": "http://192.168.1.100:8001/sse",
"enabled": true
},
"ragmir-upload": {
"type": "local",
"command": ["node", "C:\\ragmir-mcp-server\\upload-client\\upload-client.mjs"],
"environment": {
"RAGMIR_UPLOAD_URL": "http://192.168.1.100:8002/upload"
},
"enabled": true
}
}
}- Restart OpenCode.
The agent gets an upload_to_ragmir tool from the ragmir-upload MCP server. It simply calls:
upload_to_ragmir(project="my-project", path="docs/report.docx", localPath="C:\\Users\\user\\Documents\\report.docx")
The agent reads the file locally and uploads it — no code, no shell commands, no manual steps.
| Tool | Description |
|---|---|
upload_to_ragmir(project, path, localPath) |
Upload a local file to a Ragmir project |
list_local_files(directory, extensions?) |
List files in a local directory (useful for finding files to upload) |
If environment variables don't work, create config.json next to upload-client.mjs:
{
"uploadUrl": "http://192.168.1.100:8002/upload"
}The upload client uses Node.js http.request (no undici fetch), so there is no 50MB limit — files of any size can be uploaded.
- Go to Admin Settings → Connections → OpenAPI Servers
- Click Add OpenAPI Server
- Enter:
- Name:
Ragmir - URL:
http://192.168.1.100:8000/ragmir - API Key:
your-api-key
- Name:
- Click Save
| File | Description |
|---|---|
/usr/local/lib/ragmir-server/server.js |
MCP server |
/etc/ragmir/mcpo-config.json |
mcpo configuration |
/etc/systemd/system/ragmir-mcp.service |
systemd service |
/opt/ragmir-projects/ |
Project storage |
# Service
systemctl status ragmir-mcp
systemctl restart ragmir-mcp
journalctl -u ragmir-mcp -f
# SSE gateway (ragmir MCP)
systemctl status ragmir-sse
systemctl restart ragmir-sse
journalctl -u ragmir-sse -f
# SSE gateway (ragmir-upload MCP, optional)
systemctl status ragmir-upload-mcp
systemctl restart ragmir-upload-mcp
journalctl -u ragmir-upload-mcp -f
# Logs
journalctl -u ragmir-mcp -n 50 --no-pager# Check Node.js version
node --version # Must be >= 22
# Check logs
journalctl -u ragmir-mcp -n 50
# Check mcpo
curl http://localhost:8000/docs# Check if port is listening
ss -tlnp | grep 8000
# Check firewall
sudo ufw allow 8000/tcp
# Check API key
curl -H "Authorization: Bearer YOUR_KEY" http://localhost:8000/ragmir/ragmir_list_projects -X POST -d '{}'Projects are stored in /opt/ragmir-projects/. Create via MCP:
curl -X POST http://localhost:8000/ragmir/ragmir_create_project \
-H "Authorization: Bearer CHANGE-ME" \
-H "Content-Type: application/json" \
-d '{"name":"my-project"}'The MCP server delegates to the global @jcode.labs/ragmir package. Issues in
that package that affect this server are tracked here.
Symptom. rgr ingest exits 0 but every PDF emits
Parsing: pdf.destroy is not a function; the per-file errors counter in
the run summary increments.
Cause. Upstream packages/ragmir-core/src/parsing.ts:686 calls
await pdf.destroy() unconditionally in a finally block. unpdf@1.8.0
removed PDFDocumentProxy.destroy() from the returned proxy. Upstream
packages/ragmir-core/package.json declares "unpdf": "^1.4.0", which
allowed the breaking bump. Verified at runtime:
unpdf@1.6.2—typeof pdf.destroy === 'function',await pdf.destroy()resolves cleanly.unpdf@1.8.0—typeof pdf.destroy === 'undefined', calling it throwspdf.destroy is not a function.
Reported. jcode-works/jcode-ragmir#158
Workaround. Apply the local one-line patch via the idempotent script:
scripts/apply-rgr-pdf-patch.shThe script wraps the call in a type guard:
- await pdf.destroy();
+ if (typeof pdf.destroy === "function") await pdf.destroy();It is idempotent (running it again is a no-op) and runs automatically as
the postinstall hook of this repo (package.json). If upstream ships a fix
(pin to unpdf ~1.6.x or a defensive guard in parsing.ts), restore the
original file and delete the script:
# List backups
ls /usr/local/node22/lib/node_modules/@jcode.labs/ragmir/dist/parsing.js.bak.*
# Restore
cp /usr/local/node22/lib/node_modules/@jcode.labs/ragmir/dist/parsing.js.bak.<timestamp> \
/usr/local/node22/lib/node_modules/@jcode.labs/ragmir/dist/parsing.jsSymptom. Any .yaml / .yml file containing more than one YAML
document (the Hugo / Jekyll / Eleventy / Hexo / Pelican frontmatter
pattern) fails to ingest with:
YAML.parse() ERROR: Source contains multiple documents;
please use YAML.parseAllDocuments() at line 5, column 1
The file is silently skipped — indexedFiles does not increment for it
and rgr audit reports it as missing from the index.
Cause. Upstream packages/ragmir-core/src/parsing.ts:86 (compiled
dist/parsing.js) handles .yaml / .yml via YAML.parse(). The
yaml npm package (v2.9.0) refuses multi-document sources in parse()
and points at parseAllDocuments() for that case.
Reported. jcode-works/jcode-ragmir#159
Workaround. Apply the local multi-line patch via the idempotent script:
scripts/apply-rgr-yaml-patch.shThe script replaces the single YAML.stringify(YAML.parse(...)) call
with YAML.parseAllDocuments(...) + map + join("\n"):
- text = YAML.stringify(YAML.parse(await readFile(file.absolutePath, "utf8")));
+ const docs = YAML.parseAllDocuments(await readFile(file.absolutePath, "utf8"));
+ text = docs.map((d) => YAML.stringify(d)).join("\n");Both documents (frontmatter + body) become indexable text and the file
ingests cleanly. Empty / frontmatter-only documents stringify to null
and are handled by the existing downstream null-text path.
MIT
{ "mcp": { "ragmir": { "type": "remote", "url": "http://192.168.1.100:8001/sse", "enabled": true }, "ragmir-upload": { "type": "remote", "url": "http://192.168.1.100:8003/sse", "enabled": true } } }