MCP Platform

by michaelyagi

Not rated
GitHub

About

Local MCP runtime with multi-agent orchestration, distributed tool servers, and ML-powered media recommendations.

Details

Author
michaelyagi
Categories
Productivity

Setup

Install MCP Platform in your MCP client (Claude Desktop, Cursor, Windsurf, and others).

Repository: https://github.com/michaelyagi/mcp-platform

Follow the installation instructions in the repository README, then restart your MCP client.

Local MCP runtime with multi-agent orchestration, distributed tool servers, ML-powered media recommendations, persistent cross-session memory, and a proactive agent scheduler.

⚠️Experimental— intended for personal and experimental use only, not for production deployment.

- Prerequisites
-
1. Quick Start
-
2. Using MCP Servers with Other Clients
-
3. Client Configuration
-
4. Adding Custom Tools
-
5. Distributed Mode (A2A Protocol)
-
6. Testing
-
7. Architecture
-
8. RAG & Conversation Memory
-
9. Persistent Memory & Proactive Agents
-
10. Intent Patterns & Troubleshooting
-
License

- Python 3.12+
- 16GB+ RAM recommended
- Ollama installed

cd mcp-platform python3 -m venv .venv source .venv/bin/activate # Linux/macOS .venv\Scripts\activate # Windows PowerShell pip install -r requirements.txt

WSL2 note:creating.venvinside/mnt/c/fails because NTFS does not support the Unix symlinks that venvs require. Create the venv in the Linux filesystem instead:

python3 -m venv /home/$USER/.virtualenvs/mcp_platform source /home/$USER/.virtualenvs/mcp_platform/bin/activate pip install -r requirements.txt
curl -fsSL https://ollama.com/install.sh | sh OLLAMA_HOST=0.0.0.0:11434 ollama serve ollama pull qwen2.5:14b-instruct-q4_K_M # primary inference (recommended — any tool-calling model works) ollama pull qwen2.5:0.5b # routing classifier (fast pre-flight intent detection) ollama pull bge-large # RAG document embeddings ollama pull qwen3-vl:8b-instruct # vision / image analysis

GGUF alternative:local GGUF files (e.g. from Hugging Face) work too — download the file and run:gguf add <path_to_file>in the prompt. No config editing needed.

sentence-transformers models(memory embeddings + RAG reranker) download automatically on first use — no manual pull needed.

Access web UI at:http://localhost:9000/client/ui/index.html

Add to your MCP client config (e.g.,claude_desktop_config.json):

{ "mcpServers": { "code_assistant": { "command": "/path/to/mcp-platform/.venv/bin/python", "args": ["/path/to/mcp-platform/servers/code_assistant/server.py"] } } }

Windows paths:"command": "C:\\path\\to\\mcp-platform\\.venv\\Scripts\\python.exe"

- code_assistant- AI-powered code analysis, generation, and refactoring (12 tools)
- code_review- Code review, search, and bug fixing (3 tools)
- code_runner- Python/bash execution sandbox (4 tools)
- discord- Discord channel notifications via webhook (2 tools) ⚠️RequiresDISCORD_WEBHOOK_URL
- github- GitHub repo clone, browse, and cleanup (4 tools) ⚠️RequiresGITHUB_TOKENfor private repos
- google- Gmail + Google Calendar (13 tools) ⚠️Requires Google setup (Apps Script or OAuth — seeGoogle Setup)
- image- Image search, analysis, and AI generation (6 tools) ⚠️RequiresSERPER_API_KEYfor search; generation is free
- location- Weather, time, location (3 tools) — uses Open-Meteo (free, no key); falls back to OpenWeatherMap ifOPENWEATHER_API_KEYis set
- plex- Media library + ML recommendations (18 tools) ⚠️RequiresPLEX_URL,PLEX_TOKEN
- rag- Vector search and management (8 tools) ⚠️Requires Ollama +bge-large
- system- System info and processes (3 tools)
- text- Text processing and web search (8 tools)
- trilium- Trilium notes integration (11 tools) ⚠️RequiresTRILIUM_URL,TRILIUM_TOKEN

# === LLM Backend === OLLAMA_BASE_URL=http://127.0.0.1:11434 # Use 127.0.0.1 for local; LAN IP requires OLLAMA_HOST=0.0.0.0 on the server OLLAMA_VISION_MODEL=qwen3-vl:8b-instruct MAX_MESSAGE_HISTORY=30 LLM_MESSAGE_WINDOW=15 # Sliding window of messages sent to LLM; older turns fall into Conversation RAG LLM_TEMPERATURE=0.3 OLLAMA_NUM_CTX=8192 # KV cache / context window size OLLAMA_NUM_PREDICT=4096 # Max tokens the LLM will generate per response OLLAMA_REPEAT_PENALTY=1.1 # Penalise token repetition (1.0 = disabled) IMAGE_MODEL=flux # Model used for AI image generation # === GGUF Configuration === GGUF_GPU_LAYERS=-1 GGUF_CONTEXT_SIZE=4096 GGUF_BATCH_SIZE=512 # === API Keys === PLEX_URL=http://localhost:32400 PLEX_TOKEN=your_token_here TRILIUM_URL=http://localhost:8888 TRILIUM_TOKEN=your_token_here SHASHIN_BASE_URL=http://localhost:6624/ SHASHIN_API_KEY=your_key_here SERPER_API_KEY=your_key_here OLLAMA_TOKEN=your_token_here LANGSEARCH_API_KEY=your_key_here # fallback search when Ollama weekly limit is reached OPENWEATHER_API_KEY=your_key_here # fallback weather when Open-Meteo is unavailable DISCORD_WEBHOOK_URL=your_webhook_url_here # === Google Apps Script (alternative to OAuth for Gmail/Calendar) === # Paste PASTE_INTO_GOOGLE_APPS_SCRIPT.js into script.google.com, deploy as a Web App, # then set the URL with your SECRET_KEY appended as ?key=... # When set, ALL Google tools use the script instead of OAuth. GOOGLE_APPS_SCRIPT_URL=https://script.google.com/macros/s/.../exec?key=<strong-random-secret-32-chars> # === A2A Protocol === A2A_ENDPOINTS=http://localhost:8010 A2A_EXPOSED_TOOLS= # === Performance Tuning === CONCURRENT_LIMIT=3 EMBEDDING_BATCH_SIZE=50 DB_FLUSH_BATCH_SIZE=50 # === Tool Control === DISABLED_TOOLS=plex: # === Location === DEFAULT_CITY=Vancouver DEFAULT_STATE=BC DEFAULT_COUNTRY=Canada DEFAULT_TIMEZONE=America/Vancouver
ollama pull qwen2.5:14b-instruct-q4_K_M # primary inference — any tool-calling model works; 14b q4 is a good default ollama pull qwen2.5:0.5b # routing classifier — small/fast; set LLM_ROUTING_MODEL=qwen2.5:0.5b in .env ollama pull bge-large # required for RAG ollama pull qwen3-vl:8b-instruct # required for image tools # sentence-transformers models (memory embeddings + reranker) auto-download on first use

GGUF files:download any GGUF (e.g. from Hugging Face) and run:gguf add <path_to_file>in the prompt — no config editing needed.

OLLAMA_VISION_MODEL=qwen3-vl:8b-instruct DISABLED_TOOLS=plex:,image_tools:shashin_analyze,shashin_random,shashin_search OLLAMA_TOKEN=<token> SERPER_API_KEY=<key>
:jobs - List all scheduled jobs :jobs pause <label> - Pause a scheduled job :jobs enable <label> - Resume a scheduled job :jobs cancel <label> - Delete a scheduled job :jobs info <label> - Show full job detail :memory - List all memories :memory semantic - List permanent memories only :memory episodic - List session-derived memories :memory forget <id> - Delete a memory by ID :memory clear - Clear all episodic memories :memory clear session <id> - Delete memories from one session :memory consolidate <id> - Extract memories from a session now :memory add <fact> - Manually add a permanent memory :memory dedup - Remove duplicate memories :commands - List all available commands :clear sessions - Clear all chat history :clear session <id> - Clear session :sessions - List all sessions :stop - Stop current operation :stats - Show performance metrics :tools - List available tools :tools --all - Show all tools including disabled :tool <n> - Get tool description :model - List all available models :model <n> - Switch to a model :gguf add <path> - Register a GGUF model :gguf remove <alias> - Remove a GGUF model :gguf list - List registered GGUF models :a2a on/off/status - Control A2A mode :health - Health overview of all servers :env - Show environment configuration

Option A — Google Apps Script (simpler setup)
- Go to
https://script.google.comand create a new project
- Delete all existing code and paste the contents ofPASTE_INTO_GOOGLE_APPS_SCRIPT.js
- Replace<SECRET_KEY>in the script with a strong random secret (32+ characters). Generate one:python3 -c "import secrets; print(secrets.token_urlsafe(32))"Anyone who knows this key can read your emails and calendar — treat it like a password.
- ClickDeploy > New deployment > Web app

- Execute as:Me
- Who has access:Anyone
- ClickDeployand copy the Web App URL

GOOGLE_APPS_SCRIPT_URL=<Web App URL>?key=<your SECRET_KEY>

On subsequent script edits, useDeploy > Manage deploymentsand edit the existing deployment — do not create a new one or the URL will change.

One-time setup. After completing these steps the server runs headlessly.
- Go to
https://console.cloud.google.com/and create a project
- EnableGmail APIandGoogle Calendar API
- Create an OAuthDesktop appclient and downloadcredentials.json
- Place atservers/google/credentials.json
- Publish app toIn Production— this is required. Apps left in "Testing" mode have tokens that expire every 7 days, causinginvalid_granterrors on scheduled jobs
- Run:.venv/bin/python auth_google.py
- Restart:python client.py

If the token expires later:the platform detects it on the next Google tool call and shows a re-authorisation banner in the UI with a link. Click the link, approve access, paste the authorisation code into the chat — no server restart needed. Alternatively, deleteservers/google/token.jsonand re-run step 6.

mkdir servers/my_tool && touch servers/my_tool/server.py
from mcp.server.fastmcp import FastMCP from tools.tool_control import check_tool_enabled from client.tool_meta import tool_meta mcp = FastMCP("my-tool-server") @mcp.tool() @check_tool_enabled(category="my_tool") @tool_meta( tags=["read", "search"], triggers=["my keyword", "my phrase"], template='use my_function: arg1=""', ) def my_function(arg1: str) -> str: """Short description.""" return json.dumps({"content": f"Processed {arg1}"}) if __name__ == "__main__": mcp.run(transport="stdio")

Restart the client and the tool is live — routed, badged, and registered automatically.

Createexternal_servers.jsonin the project root:

{ "external_servers": { "deepwiki": { "transport": "sse", "url": "https://mcp.deepwiki.com/mcp", "enabled": true } } }

Supported transports:sse,http,stdio. Header auth env var convention:

python a2a_server.py # Terminal 1 — starts on http://localhost:8010 python client.py # Terminal 2
A2A_ENDPOINTS=http://localhost:8010,http://gpu-server:8020 A2A_EXPOSED_TOOLS=plex,location,text # empty = expose all

All configured endpoints are discovered and registered concurrently at startup viaasyncio.gather()— connection timeouts for unreachable endpoints no longer block each other.

Activate your virtualenv first, then run from the project root:

source .venv/bin/activate # Linux/macOS .venv\Scripts\activate # Windows PowerShell # WSL2: source /home/$USER/.virtualenvs/mcp_platform/bin/activate python -m pytest # all tests python -m pytest -m unit # fast unit tests only python -m pytest -m integration # integration tests python -m pytest -m e2e # end-to-end tests python -m pytest --no-cov # skip coverage (faster) python -m pytest -x # stop on first failure python -m pytest -k "session" # filter by name
tests/ ├── conftest.py ├── unit/ <- fast isolated unit tests ├── integration/ <- multi-component tests └── e2e/ <- full conversation tests tests/results/ <- generated after running tests ├── junit.xml ├── coverage.xml ├── test-report.html └── coverage-report.html

Tests run automatically on every push and pull request via GitHub Actions (.github/workflows/ci.yml). On failure, a GitHub Issue is opened automatically with a link to the failed run.

To upload coverage to Codecov, add to the workflow:

- name: Upload coverage uses: codecov/codecov-action@v3 with: files: results/coverage.xml
servers/ ├── code_assistant/ 12 tools - AI-powered code analysis, generation, and refactoring ├── code_review/ 3 tools - Code review, search, and bug fixing ├── code_runner/ 4 tools - Python/bash execution sandbox ├── discord/ 2 tools - Discord channel notifications [requires DISCORD_WEBHOOK_URL] ├── github/ 4 tools - GitHub repo clone, browse, and cleanup ├── google/ 13 tools - Gmail + Google Calendar [requires Google setup] ├── image/ 6 tools - Image search, analysis, AI generation ├── location/ 3 tools - Weather, time, location ├── plex/ 18 tools - Media + ML recommendations [requires PLEX_URL + PLEX_TOKEN] ├── rag/ 8 tools - Vector search and management [requires Ollama + bge-large] ├── system/ 3 tools - System info and processes ├── text/ 8 tools - Text processing and web search └── trilium/ 11 tools - Trilium notes integration [requires TRILIUM_URL + TRILIUM_TOKEN]

The platform usesasyncio.gather()at several layers to run non-LLM work concurrently:

Hardware note:LLM inference serialises at the Ollama GPU layer regardless of concurrency — one inference runs at a time on a single GPU. The parallelism benefit is in I/O-bound work: HTTP tool calls, database queries, and network requests. True parallel LLM execution would require multiple GPUs or cloud-hosted sub-agents.

mcp-platform/ ├── servers/ ├── a2a_server.py ├── client.py ├── client/ │ ├── ui/ │ ├── capability_registry.py <- auto-populated from @tool_meta │ ├── langgraph.py │ ├── memory_consolidator.py <- persistent cross-session memory │ ├── proactive_agent.py <- scheduler + condition triggers │ ├── query_patterns.py <- auto-populated from @tool_meta triggers │ ├── tool_meta.py <- single source of truth for tool metadata │ ├── websocket.py │ └── ... ├── data/ │ ├── sessions.db <- session + message history │ ├── memory.db <- persistent memory (created on first run) │ └── scheduler.db <- scheduled jobs (created on first run) └── tools/

Every LLM call receives context assembled in this exact order:

System prompt ├─ Persistent memory — top 5 by importance (always injected) ├─ Persistent memory — query-relevant (vector search, above threshold) └─ Original system instructions + session ID Conversation RAG — turns that scrolled out of the window (if relevant) Message window — last LLM_MESSAGE_WINDOW turns (always injected) Current user message

Lookup chain — what happens on every query:
-

Persistent memory (always)— top 5 highest-importance memories are injected unconditionally into the system prompt, regardless of query relevance. This ensures core facts (your name, family, preferences) are never lost even when no query scores well. Additional query-relevant memories are added on top via vector search.

Message window (always)— the lastLLM_MESSAGE_WINDOWturns of the current session are included directly as conversation history.

Conversation RAG (always)— a semantic search runs against turns that have scrolled out of the window. Results above the reranker threshold are injected between the system prompt and history.

LLM generates responseusing all of the above.

Conversation window (LLM_MESSAGE_WINDOW)

Controls how many recent turns the LLM sees directly. Set in.env:

LLM_MESSAGE_WINDOW=15 # default: 6, recommended: 15

A window of 6 is too tight for normal conversation — information shared early in a session scrolls out before you can ask about it. 15 covers a full back-and-forth without hitting token limits on qwen2.5:14b. If you share something and the LLM seems to forget it a few messages later, increase this value.

When history exceeds the window, older turns are automatically ingested into the RAG vector database asHuman + Assistantpairs. They remain searchable via semantic similarity even after scrolling out of the window.

On every message, a semantic search runs against the full RAG store using the current user message as the query. Matching chunks (from old conversation turns, ingested documents, Plex subtitles, or research) are injected into context automatically — no explicitsearch ragtrigger needed.

Added to theragserver. Queries the session SQLite database directly for ordered, timestamped message history. The current session ID is always injected into the system prompt so the LLM can pass it through automatically.

use session_history_tool: session_id="<id>" [limit="20"] [order="asc"]

Triggers:first prompt,what did I ask,earlier in this session,summarise this session,session history

9. Persistent Memory & Proactive Agents

The platform has three layers of context, each serving a different purpose:

What this means in practice:If you tell the platform your son's name and ask about it 3 messages later, the message window handles it. If you ask 20 messages later, RAG handles it (usually). If you start a new session tomorrow, only persistent memory has it.

Step 1 — Have a conversation.Tell the platform things you want it to remember: your name, your family, your projects, your preferences. The more declarative the better ("My wife's name is Suzy" vs "what's my wife's name?").

Step 2 — Memory extracts automatically.After 15 minutes of inactivity, theInactivityWatcherfires and runs the LLM over your session transcript. It extracts facts and stores them indata/memory.dbwith vector embeddings.

Re-consolidation is smart: it tracks message count at last consolidation and only re-runs if new messages have been added since. Going idle overnight triggers one extraction, not dozens.

Step 3 — Memories inject on every query.On each new message, two things happen: the top 5 highest-importance memories are always injected into the system prompt unconditionally (so your name, family, and key preferences are never forgotten), then a vector search finds additional query-relevant memories and adds them on top. The combined block looks like this:

## Persistent Memory (from past sessions) The following facts are KNOWN and TRUE. Use them to answer directly. ◆ The user's name is Bob ○ Bob's wife is Suzy, a nurse and excellent cook ○ Bob's son Sam is 11, plays accordion, excels at hockey ...

Step 4 — Memories accumulate over time.Episodic memories accessed 3+ times are promoted to semantic (permanent) tier nightly. The platform gets more useful the longer you use it.

When memory doesn't fire automatically

The inactivity watcher fires 15 minutes after your last message. If you need memories extracted immediately:

Use:sessionsto find the session ID. The command clears the consolidation flag and re-runs extraction regardless of message count.

:memory — list all memories (sorted by relevance) :memory semantic — permanent memories only :memory episodic — session-derived memories :memory forget <id> — delete one memory by ID :memory clear — delete all episodic memories :memory clear session <id> — delete memories from one session :memory consolidate <id> — extract memories from a session now :memory add <fact> — manually add a permanent memory :memory dedup — remove duplicate memories

Manually added memories (:memory add) are stored assemantictier with importance 1.0 — they always rank first in retrieval.

If the LLM forgets something mid-session

IncreaseLLM_MESSAGE_WINDOWin.env. The default of 6 is too tight — 15 is recommended. Information shared early in a session scrolls out of the window before you can ask about it.

Once a turn scrolls out of the window it moves intoConversation RAG— it's still there, but now retrieved by semantic similarity rather than direct context. This means the query phrasing needs to be close enough to the original content for the reranker to surface it. If the LLM still can't find something that was said earlier in the same session, try rephrasing the question to use the same keywords as the original statement.

No reviews yet — be the first

Sign in to leave a review

Use Google, GitHub, or an email account so ratings stay tied to real people.

Email sign in

No reviews posted yet.