This site covers how to use each service after it's set up. For installation steps, see the setup guide. For source code and architecture, see the bundle README.
- Top bar on every service β each gated subdomain shows a thin top bar with the service icon (same as the home page card), a sync indicator, your signed-in user, and a Logout link. The actual service runs in an iframe below the bar so the bar stays visible on every navigation.
terminal.pocketcode.inhas its own tabbed variant. - Logout is cross-tab β click Logout on any service tab and every other open tab (home, other services) shows a Session expired overlay within a few seconds. Click Sign in on the overlay and you return to that tab's URL after login, not the home page.
- Browser back button works within a service β back/forward retrace your navigation inside the iframe instead of jumping out of the wrapper. (Same-iframe SPA routing inside Sim.ai still doesn't update the address bar β that's an upstream limitation.)
- www.* aliases β typing
www.openclaw.pocketcode.in(or any otherwww.variant) lands on the bare hostname with the path preserved. HTTP requests also redirect to HTTPS automatically. - Service icons in the browser tab β each subdomain has its own favicon so you can tell tabs apart at a glance. Home/login use π.
- Service Workers blocked β if a service registers a Service Worker (OpenClaw, Sim.ai PWAs), the wrapper unregisters it on load and Caddy 404s
/sw.js. PWA install is sacrificed; reliable wrapper rendering is gained.
All Services at a Glance
These cards are reference only β they describe each service. To launch any service, go to pocketcode.in.
claude, docker, and ai-doctor on PATH.Infrastructure Reference
sim-db-1: simstudio, n8n, openclaw, ailab, ollama_results, and voice_pg (v2 voice agent data plane).ai-stack Docker network. Each service is reachable by its container name./shared and a bind-mount at /uploads.How to Read This Documentation
Each service tab follows the same structure:
- Quick Start β login URL + first thing to do after logging in
- Common Tasks β the 5-10 most useful workflows
- This Setup's Quirks β anything specific to how it's configured here
- Resources β official docs and references
.pocketcode.in cookie that protects every subdomain via Caddy's forward_auth. Auto-SSO goes further: the gateway also programmatically logs you into Sim.ai, Open WebUI, and n8n using stored credentials β click any card and you're already inside. A watchdog refreshes those service sessions every 2 minutes and on tab focus, so if you accidentally click a service's own "Logout" or its session expires, you're silently re-logged-in within seconds.
forward_auth immediately blocks access to every *.pocketcode.in service β visiting any of them redirects to the pocketcode login page. After re-logging in, every service is instantly accessible again (since their own sessions never died).
TLS Health Quick Check
If you ever notice intermittent TLS errors in private browser windows or when curling from outside the server, this is the first command to run:
for h in pocketcode.in sim.pocketcode.in chat.pocketcode.in n8n.pocketcode.in \
openclaw.pocketcode.in pgadmin.pocketcode.in terminal.pocketcode.in \
qdrant.pocketcode.in ollama.pocketcode.in openrouter.pocketcode.in; do
cn=$(echo Q | timeout 5 openssl s_client -servername "$h" -connect 127.0.0.1:443 2>/dev/null \
| openssl x509 -noout -subject 2>/dev/null | sed -E 's/.*CN ?= ?//')
printf " %-30s %s\n" "$h" "${cn:-NO CERT}"
done
Each hostname should report a CN β typically *.pocketcode.in (the wildcard cert) or the apex name. Any NO CERT line means Caddy isn't serving TLS for that hostname and the setup guide's Tab 18 (TLS Troubleshooting) has the full diagnostic + recovery playbook.
Single command that summarizes ongoing cert health:
docker exec caddy find /data/caddy/certificates -name '*.crt' 2>/dev/null | sort
You should see a wildcard_.pocketcode.in/ entry and an apex entry. Old per-subdomain certs may linger for ~30 days β harmless leftovers.
_acme-challenge.pocketcode.in are the single most common cause of cert acquisition failures. Quick check: dig +short TXT _acme-challenge.pocketcode.in. If it returns more than one record, recovery is needed β setup guide Tab 18 Step 4.
Quick Start
- Go to chat.pocketcode.in
- Sign in with your Open WebUI admin account (first signup = admin)
- Pick a model from the dropdown at the top center of the chat window
- Type a message β press Enter
Pull a New Model
Models are pulled inside the Ollama container. You can do this from Open WebUI's admin panel OR via the web terminal:
docker exec ollama ollama pull llama3.2:3b docker exec ollama ollama pull qwen2.5:7b docker exec ollama ollama pull nomic-embed-text # for RAG embeddings
Or in Open WebUI: Settings β Admin Panel β Models β Pull a model from Ollama.com. Type the model name (e.g. llama3.2:3b) and click pull.
Common Tasks
Upload a document for Q&A (RAG):
- Click the π paperclip icon in the chat input
- Select a PDF, DOCX, MD, or TXT file
- Ask questions about it β the model reads the document and answers
Build a permanent knowledge base:
- Profile (top right) β Workspace β Knowledge β + Create Knowledge
- Name it (e.g. "Company Docs") β upload multiple files
- Open WebUI chunks the docs, embeds them with
nomic-embed-text, stores in ChromaDB - In any chat, reference the collection with
#Company Docs
Create a custom model with a pinned system prompt:
- Workspace β Models β + Create a model
- Choose base model (e.g. llama3.2), add a system prompt, save
- Now appears in the chat model dropdown
Use as OpenAI-compatible API for other tools:
curl https://chat.pocketcode.in/api/chat/completions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "llama3.2:3b",
"messages": [{"role":"user","content":"Hello"}]
}'
API key is in Settings β Account β API Keys. Click "Create new secret key".
Useful Settings
| Setting | Where | What it does |
|---|---|---|
| Default model | Settings β General | Picked when you start a new chat |
| Embedding model | Admin β Settings β Documents | Set to nomic-embed-text for RAG |
| System prompt | Per-chat βοΈ icon | Pin instructions for that conversation |
| Temperature | Per-chat βοΈ β Advanced | 0 = deterministic, 1 = creative |
| User signups | Admin β Settings β General | Enable/disable. Default is "pending approval" |
This Setup's Quirks
http://ollama:11434 (internal Docker DNS). Open WebUI sees all your Ollama models automatically.
Resources
Quick Start
- Go to sim.pocketcode.in
- Sign up (first time) or sign in
- Click + New Workflow
- Drag blocks from the left panel onto the canvas, connect them with lines
- Click Run in the top right to test
Block Types
| Block | What it does | Common use |
|---|---|---|
| Agent | LLM call with optional tools | Most workflows start here |
| Function | Run JavaScript code | Data transformation between steps |
| API | HTTP request to any URL | Call external services |
| Condition | If/else branching | Route based on agent output |
| Loop | Iterate over array | Process lists of items |
| Schedule | Cron trigger | Run workflow on schedule |
| Webhook | HTTP endpoint trigger | External app calls Sim.ai |
Connecting to Models
Inside any Agent block:
- Local Ollama: Provider = Ollama, base URL is auto-set to
http://ollama:11434 - Cloud models via OpenRouter: Provider = OpenAI, base URL =
http://openrouter-proxy:4000, API key = whatever you set in LiteLLM config (or leave blank if disabled) - Direct Claude API: Provider = Anthropic, paste your Anthropic API key
Build Your First Workflow β Hello World
- Drag Agent block onto canvas
- Set model:
llama3.2:3b - Set system prompt: "You are a helpful assistant."
- Set user message: "What is 2+2?"
- Click Run β see the output in the right panel
Add Tools to an Agent
Tools let agents take actions. Click an Agent block β Tools tab β add:
- HTTP request β agent can call any URL
- JavaScript β agent can execute code
- Knowledge base β agent can query a Qdrant collection
- Memory β agent can read/write to short-term memory
Common Tasks
Schedule a workflow to run daily:
- Add a Schedule block β set cron (e.g.
0 9 * * *= daily 9am) - Connect it to your first Agent block
- Click Deploy in top right
- Workflow now runs automatically
Expose workflow as a webhook (for external apps):
- Add a Webhook block as the trigger
- Deploy β copy the webhook URL
- Any service can POST JSON to it β triggers the workflow
This Setup's Quirks
sim-db-1 (pgvector image) which also hosts databases for n8n, OpenClaw, and Open WebUI. Inspect via pgAdmin β connect to host sim-db-1, port 5432.
sim-realtime.pocketcode.in serves the live-collaboration WebSocket. If /workspace goes blank, check the subdomain's cert is valid and your browser cache is clear.
Resources
Quick Start
- Go to n8n.pocketcode.in
- Sign in with your owner account (first signup = owner)
- Click + Add workflow
- Click + in the canvas β search for a trigger (e.g. "Manual")
- Add more nodes β connect β click Execute Workflow
Node Categories
| Category | Examples | Use for |
|---|---|---|
| Triggers | Manual, Webhook, Schedule, Email, Slack | Starting events |
| AI | OpenAI, Anthropic, Ollama, OpenRouter | LLM calls |
| Apps | Slack, Discord, Gmail, GitHub, Notion | External service integration |
| Data | Postgres, HTTP Request, RSS, CSV | Read/write data |
| Logic | IF, Switch, Loop, Merge, Wait | Flow control |
| Code | Function, Code (JS/Python) | Custom transforms |
Build Your First Workflow β Slack to Ollama
- Trigger: Slack Trigger (or Webhook for testing)
- Add Ollama Chat Model node β base URL
http://ollama:11434, modelllama3.2:3b - Add AI Agent node β pass the Slack message as user input
- Add Slack Send Message node β post the AI response back
- Click Activate in top right
Common Tasks
Use Ollama directly in a workflow:
- Add Ollama Chat Model node
- Credentials β New β Base URL:
http://ollama:11434 - Pick model from dropdown
Use cloud models (Claude/GPT) via OpenRouter:
- Add OpenAI Chat Model node (yes, even for Claude β OpenRouter is OpenAI-compatible)
- Credentials β New β Base URL:
http://openrouter-proxy:4000, API key: any value (or your LiteLLM master key) - Set model name to OpenRouter format (e.g.
anthropic/claude-3.5-sonnet)
Query the shared Postgres:
- Add Postgres node
- Credentials: Host =
sim-db-1, Port =5432, DB =n8n, User/Pass from your.env - Pick operation (Select, Insert, Update)
Receive webhooks from external apps:
- Use Webhook trigger node
- n8n shows you the test + production URLs
- For production (after Activate), URL is
https://n8n.pocketcode.in/webhook/<your-path>
This Setup's Quirks
n8n-data Docker volume holds the encryption key. All your stored credentials remain readable across container restarts. Back up this volume periodically.
~/ai-stack/n8n/run-n8n.sh β should have WEBHOOK_URL=https://n8n.pocketcode.in/ so webhook URLs n8n generates point to the right place.
Resources
Quick Start
- Go to openclaw.pocketcode.in
- If first-time access, you may see a "Pair device" prompt β see "Device Pairing" below
- Type a message in the chat input β send
Device Pairing
OpenClaw uses a device pairing system. New browsers/devices need to be approved from the CLI:
docker exec -it openclaw node /app/openclaw.mjs devices list docker exec -it openclaw node /app/openclaw.mjs devices approve <UUID>
Common Tasks
Switch the active model:
- Settings (gear icon) β Models
- Choose from local Ollama or configured cloud providers
Configure cloud model providers:
- Settings β API Keys β add Anthropic / OpenAI / OpenRouter keys
- Models become available in the chat selector
Start a new agent session:
- Click + New Session (sidebar)
- Pick a model + system prompt template (or custom)
- Sessions persist in the OpenClaw database
This Setup's Quirks
http://ollama:11434 via the ai-stack Docker network. Models pulled into Ollama appear automatically.
Resources
- OpenClaw docs (if available)
claude CLI installed directly on the host, and a containerised variant under claude-code that runs the same npm package inside a sandboxed image with ~/pocketcode-project bind-mounted. The host CLI is what you'll use most days; the container is useful when you want isolated env vars or to run the agent against a frozen working copy.
Quick Start β host CLI (recommended)
Installed in ~/.claude/bin/ on the VPS. Open the web terminal (or SSH in) and:
cd ~/pocketcode-project claude
If this is the first time, you'll get a sign-in URL β open it in your local browser, paste the returned code. The CLI saves credentials under ~/.claude/; subsequent runs are instant.
Quick Start β containerised variant
Uses a long-running claude-code container with ~/pocketcode-project mounted at /workspace:
docker exec -it claude-code bash claude
First run prompts for an Anthropic API key. Paste yours from console.anthropic.com β API Keys (or sign in interactively, same as the host CLI).
Basic Usage
claudeβ start interactive session in current directoryclaude "fix the bug in main.py"β give a task directlyclaude --continueβ resume the last conversation/helpinside the session β see all commands/clearβ clear the conversation/costβ see token usage and cost
Working with the Stack
Host CLI: just cd into whichever directory matters and run claude. Everything in ~/pocketcode-project is directly accessible.
Containerised variant: ~/pocketcode-project is bind-mounted at /workspace inside the container.
cd ~/pocketcode-project/services/sim # or /workspace/services/sim in container claude "review docker-compose.prod.yml and suggest improvements"
Common Tasks
Code review a file:
claude "review operations/manage/start-all.sh and find any race conditions"
Generate a new service config:
claude "create a docker-compose.yml for a Redis instance on the ai-stack network with persistence"
Debug a failing container:
docker logs sim-simstudio-1 --tail 100 > /tmp/logs.txt claude "read /tmp/logs.txt and tell me why this container is restarting"
This Setup's Quirks
/cost regularly. Sonnet 4 is ~$3/M input + $15/M output tokens.
/workspace.
Resources
What This Is
A LiteLLM proxy that exposes an OpenAI-compatible API on port 4000. You configure cloud providers (OpenAI, Anthropic, OpenRouter.ai, etc.) once, and any tool that speaks OpenAI's API can use them.
From Inside the Stack
Containers on the ai-stack network reach the proxy at:
http://openrouter-proxy:4000
From Outside (via HTTPS)
https://openrouter.pocketcode.in
Configured Models
LiteLLM supports two configuration styles. Pick whichever matches your install:
A. Single-model CLI form β what the v1.0βv1.5 setup guide installs. Passes the model on the command line, no config file:
docker run -d --name openrouter-proxy --network ai-stack \ -e OPENROUTER_API_KEY="sk-or-v1-..." \ -p 4000:4000 \ ghcr.io/berriai/litellm:main-latest \ --model openrouter/anthropic/claude-3.5-sonnet --port 4000
Any client calling /v1/chat/completions with any model name gets routed to the single configured model. Simple, good for a one-model lab.
B. Multi-model YAML form β if you want to expose several backends (Claude, GPT-4, Llama, etc.) through one proxy and have clients choose at request time. Mount a config file:
model_list:
- model_name: claude-sonnet-4
litellm_params:
model: anthropic/claude-sonnet-4-20250514
api_key: os.environ/ANTHROPIC_API_KEY
- model_name: gpt-4o
litellm_params:
model: openai/gpt-4o
api_key: os.environ/OPENAI_API_KEY
- model_name: llama-3.3-70b
litellm_params:
model: openrouter/meta-llama/llama-3.3-70b-instruct
api_key: os.environ/OPENROUTER_API_KEY
Then replace the trailing --model β¦ CLI args in the run script with --config /app/config.yaml and mount the file with -v ~/ai-stack/openrouter-proxy/litellm-config.yaml:/app/config.yaml.
Common Tasks
Test from terminal:
curl https://openrouter.pocketcode.in/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-4",
"messages": [{"role":"user","content":"Hello"}]
}'
Add a new model:
- Edit
~/ai-stack/openrouter-proxy/litellm-config.yamlβ add a new entry - Restart the container:
docker restart openrouter-proxy - The new
model_nameis now usable everywhere
Use from Python:
from openai import OpenAI
client = OpenAI(
base_url="https://openrouter.pocketcode.in/v1",
api_key="any-string" # disabled in default config
)
response = client.chat.completions.create(
model="claude-sonnet-4",
messages=[{"role": "user", "content": "Hello"}]
)
print(response.choices[0].message.content)
This Setup's Quirks
~/ai-stack/openrouter-proxy/.env: ANTHROPIC_API_KEY=, OPENAI_API_KEY=, OPENROUTER_API_KEY=. Reference them in the YAML as os.environ/VAR_NAME.
/spend endpoint for usage stats.
Resources
What This Is
The raw Ollama API. Most users interact with Ollama via Open WebUI (recommended for chat) or via the SDK in code. This page covers direct API usage.
Endpoints
| Endpoint | Method | Purpose |
|---|---|---|
/api/tags | GET | List installed models |
/api/generate | POST | Single-turn generation |
/api/chat | POST | Multi-turn chat |
/api/embeddings | POST | Get vector embeddings |
/api/pull | POST | Download a new model |
/api/show | POST | Get model details |
Common Calls
List installed models:
curl https://ollama.pocketcode.in/api/tags
Chat completion:
curl https://ollama.pocketcode.in/api/chat -d '{
"model": "llama3.2:3b",
"messages": [{"role":"user","content":"Hello"}],
"stream": false
}'
Generate embeddings (for RAG):
curl https://ollama.pocketcode.in/api/embeddings -d '{
"model": "nomic-embed-text",
"prompt": "The text to embed"
}'
Pull a new model (from terminal, not API recommended):
docker exec ollama ollama pull llama3.2:3b docker exec ollama ollama pull qwen2.5:7b docker exec ollama ollama pull nomic-embed-text
Model Recommendations (CPU mode)
| Model | Size | Speed on KVM 8 | Use for |
|---|---|---|---|
llama3.2:3b | 2.0 GB | 12-18 tok/s | Default chat, summaries |
llama3.2:1b | 1.3 GB | 30-40 tok/s | Fast tasks, classification |
qwen2.5:7b | 4.7 GB | 4-7 tok/s | Better reasoning |
qwen2.5-coder:7b | 4.7 GB | 4-7 tok/s | Code completion |
nomic-embed-text | 274 MB | Embedding only | RAG vector embeddings |
This Setup's Quirks
ollama-data Docker volume. Survives container restarts. Inspect with docker exec ollama du -sh /root/.ollama.
OLLAMA_NUM_PARALLEL=2 in the run script if multiple users share the instance.
Resources
What This Is
A high-performance vector database for semantic search, RAG retrieval, and similarity matching. Stores embeddings (numerical representations of text, images, etc.) and finds nearest neighbors to a query vector. Bring your own embedding model β Ollama on this server, or any OpenAI-compatible cloud model via the OpenRouter proxy.
This Setup's Access Pattern
Qdrant is the only service that opens at its direct subdomain instead of through the launcher iframe wrapper used by the 8 other tools. The launch-pad card on pocketcode.in for Qdrant points straight to https://qdrant.pocketcode.in/dashboard.
/dashboard/manifest.json) that fail when loaded inside the launcher iframe β the auth_gate redirects, the cross-origin redirect gets CORS-blocked, and the sidebar component never finishes rendering. Other services don't have this issue, so they stay in the iframe wrapper.
You still get full protection and feature parity with other services:
- Caddy
auth_gatestill gates the subdomain β anonymous visitors get 302'd to the master login atpocketcode.in - A session-poll script is injected into Qdrant's HTML response via Caddy's
replace-responseplugin. It polls/verifyevery 15 seconds (and on tab focus). When your master session expires, an overlay appears in the Qdrant tab and counts down to redirect β same UX as every other service.
Web UI Layout
When you open qdrant.pocketcode.in/dashboard, the Web UI smart-routes:
- Zero collections β lands on
#/welcome(Get Started cards) - One or more collections β lands on
#/collections(collection list)
Left sidebar items:
| Item | What it's for |
|---|---|
| Welcome | Onboarding hero + getting-started cards. Shown when no collections exist. |
| Console | Run any REST API call interactively. Best place to learn the API. |
| Collections | Browse, create, inspect, snapshot, and query collections. |
| Tutorial | Built-in walkthroughs: Filtering (Beginner / Advanced / Full-Text), Multivector, Sparse, Hybrid, Multitenancy. Each tutorial creates a sample collection. |
| Datasets | Import sample data from remote snapshots β fastest way to populate a collection for experiments. |
| Access Tokens | Qdrant's JWT-based per-key access control. Disabled here since we auth at Caddy. |
Internal vs External Access
| From | URL | Auth |
|---|---|---|
Another container on ai-stack | http://qdrant:6333 (REST)http://qdrant:6334 (gRPC) | None β internal network |
| Host shell on the VPS | http://localhost:6333http://localhost:6334 | None β host loopback |
| Your browser (Web UI) | https://qdrant.pocketcode.in/dashboard | Master session cookie |
| External script via Caddy | https://qdrant.pocketcode.in/<endpoint> | Cookie header required |
ai-stack Docker network can call any Qdrant endpoint without credentials. That's by design β it's a backend service. The Caddy auth_gate is the only thing standing between the public internet and your collections. Don't expose Qdrant's ports to the host network publicly, and keep your pocketcode_session cookie safe.
The v1.10 Query API
Since Qdrant v1.10, the unified Query API at POST /collections/{name}/points/query replaces the older endpoints (/points/search, /points/recommend, /points/scroll). The same endpoint handles:
- Nearest neighbors search (default behavior)
- Search by point ID (recommend-style: "find things similar to point #42")
- Recommendations with positive/negative examples
- Scrolling through points (pagination)
- Random sampling
- Hybrid (dense + sparse) and multi-stage retrieval via
prefetch
The older endpoints still work for backward compatibility, but new code should use /points/query.
End-to-End: Create, Embed, Insert, Query
These examples run from the web terminal (or any host shell) using internal Docker networking β no auth needed. From outside the server, use https://qdrant.pocketcode.in with a valid session cookie.
1. Create a collection (768-dim vectors, cosine distance β matches nomic-embed-text):
curl -X PUT http://qdrant:6333/collections/my-docs \
-H "Content-Type: application/json" \
-d '{
"vectors": {"size": 768, "distance": "Cosine"}
}'
2. Create payload indexes BEFORE inserting data (recommended β see the callout below):
curl -X PUT http://qdrant:6333/collections/my-docs/index \
-H "Content-Type: application/json" \
-d '{"field_name": "category", "field_schema": "keyword"}'
curl -X PUT http://qdrant:6333/collections/my-docs/index \
-H "Content-Type: application/json" \
-d '{"field_name": "created_at", "field_schema": "datetime"}'
3. Get an embedding from Ollama, insert into Qdrant:
EMB=$(curl -s http://ollama:11434/api/embeddings \
-d '{"model":"nomic-embed-text","prompt":"The quick brown fox"}' \
| jq -c .embedding)
curl -X PUT http://qdrant:6333/collections/my-docs/points \
-H "Content-Type: application/json" \
-d "{
\"points\": [{
\"id\": 1,
\"vector\": $EMB,
\"payload\": {\"text\": \"The quick brown fox\", \"category\": \"animals\"}
}]
}"
4. Query (v1.10 Query API β /points/query):
QUERY_EMB=$(curl -s http://ollama:11434/api/embeddings \
-d '{"model":"nomic-embed-text","prompt":"fast animal"}' \
| jq -c .embedding)
curl -X POST http://qdrant:6333/collections/my-docs/points/query \
-H "Content-Type: application/json" \
-d "{
\"query\": $QUERY_EMB,
\"limit\": 5,
\"with_payload\": true
}"
Filtering β Where Most People Trip Up
Filters use three boolean groups combined with field conditions. The groups:
mustβ ALL conditions must match (AND)shouldβ at least one condition must match (OR)must_notβ none of the conditions can match (NOT)
Condition types (most common):
| Condition | Matches |
|---|---|
match: { value } | Exact equality on keyword / integer / bool field |
match: { any: [...] } | Field equals ANY value in list (OR within field) |
match: { except: [...] } | Field NOT in list |
range: { gte, lte, gt, lt } | Numeric range |
match: { text: "phrase" } (v1.15+) | Full-text phrase match (requires text payload index) |
datetime_range: { gte, lte } (v1.8+) | Datetime range queries |
geo_bounding_box, geo_radius, geo_polygon | Geographic queries |
is_empty, is_null, has_id | Existence / identity checks |
Filter inside a query β find similar vectors but only in category animals, excluding inactive ones:
curl -X POST http://qdrant:6333/collections/my-docs/points/query \
-H "Content-Type: application/json" \
-d "{
\"query\": $QUERY_EMB,
\"limit\": 5,
\"with_payload\": true,
\"filter\": {
\"must\": [ {\"key\": \"category\", \"match\": {\"value\": \"animals\"}} ],
\"must_not\": [ {\"key\": \"status\", \"match\": {\"value\": \"inactive\"}} ]
}
}"
Nested payload fields use dot notation: {"key": "metadata.author", "match": {"value": "alice"}}.
Payload Indexes β The Performance Multiplier
If you filter on a payload field, you almost certainly want a payload index on it. Without one, Qdrant filters by scanning every point match β slow on large collections.
| Schema | Use for |
|---|---|
keyword | Tags, categories, string IDs β exact-match strings |
integer | Numeric IDs, counts |
float | Numeric measurements |
bool | True/false flags |
datetime (v1.8+) | Timestamps for range queries |
geo | Lat/long pairs |
text | Free-text fields (enables phrase + tokenized matching) |
uuid (v1.11+) | UUID-typed IDs |
Python Client (qdrant-client)
The official Python library is idiomatic and matches the REST API closely:
from qdrant_client import QdrantClient
from qdrant_client.models import (
VectorParams, Distance, PointStruct,
Filter, FieldCondition, MatchValue,
)
# From another container on ai-stack network:
client = QdrantClient(host="qdrant", port=6333)
# From host shell:
# client = QdrantClient(host="localhost", port=6333)
# 1. Create collection
client.create_collection(
collection_name="my-docs",
vectors_config=VectorParams(size=768, distance=Distance.COSINE),
)
# 2. Create payload index (before inserting)
client.create_payload_index(
collection_name="my-docs",
field_name="category",
field_schema="keyword",
)
# 3. Upsert points
client.upsert(
collection_name="my-docs",
points=[
PointStruct(
id=1,
vector=embedding_768, # list of 768 floats from your embedding model
payload={"text": "...", "category": "animals"},
),
],
)
# 4. Query (unified Query API)
result = client.query_points(
collection_name="my-docs",
query=query_embedding_768,
limit=5,
query_filter=Filter(must=[
FieldCondition(key="category", match=MatchValue(value="animals")),
]),
).points
for hit in result:
print(hit.id, hit.score, hit.payload["text"])
Common Tasks
List all collections:
curl http://qdrant:6333/collections
Collection info (size, indexed fields, vector params, point count):
curl http://qdrant:6333/collections/my-docs
Scroll through all points (paginated iteration):
curl -X POST http://qdrant:6333/collections/my-docs/points/scroll \
-H "Content-Type: application/json" \
-d '{"limit": 100, "with_payload": true, "with_vector": false}'
The response includes next_page_offset; pass it as offset on the next call to continue.
Delete a collection:
curl -X DELETE http://qdrant:6333/collections/my-docs
Snapshot a collection (for backup / portability):
curl -X POST http://qdrant:6333/collections/my-docs/snapshots
Embedding Models in This Stack
| Model | Vector size | Run via | Best for |
|---|---|---|---|
nomic-embed-text | 768 | Ollama | General-purpose, fast, on-server (default choice) |
all-minilm | 384 | Ollama | Smaller, faster, lower quality |
mxbai-embed-large | 1024 | Ollama | Higher quality, slower |
text-embedding-3-small | 1536 | OpenRouter (cloud) | Best quality / price ratio |
text-embedding-3-large | 3072 | OpenRouter (cloud) | Highest quality, costs more |
| FastEmbed (BM25, ColBERT) | varies | qdrant-client[fastembed] | Sparse vectors for hybrid search |
size: 768 and then try to insert a 1536-dim vector from OpenAI's text-embedding-3-small, Qdrant rejects the insert. Pick one model per collection, or use named-vector collections for multi-vector workflows.
Hybrid Search (Dense + Sparse)
Qdrant supports hybrid retrieval: combine a dense semantic vector with a sparse keyword/BM25 vector and fuse the rankings. The v1.10 Query API uses prefetch with reciprocal rank fusion or distance-based reranking. The built-in Hybrid Search tutorial in the dashboard (Tutorial β Hybrid Search) walks through the full setup with sample data.
Multi-Tenancy
For SaaS-style use cases where one collection serves multiple isolated tenants, the recommended pattern:
- One large collection (not one per tenant β fewer indexes, better RAM use)
- Every point has a
tenant_idin payload - Create a
keywordpayload index ontenant_id - Every query filters by tenant:
filter: {"must": [{"key": "tenant_id", "match": {"value": "..."}}]}
Built-in tutorial: dashboard β Tutorials β Multitenancy.
Persistence
The qdrant container in this stack mounts a Docker volume at /qdrant/storage. Collections survive container restarts and reboots. To wipe everything (destructive β backs up nothing):
docker stop qdrant docker rm qdrant docker volume rm qdrant-data bash ~/ai-stack/run-qdrant.sh
voice_calls collection. Its indexing_threshold was lowered so even its small vector set builds a real HNSW index (Qdrant skips indexing tiny collections by default). With the index present, the dashboard's Graph / Visualize views now render for voice_calls instead of showing nothing.
Resources
Quick Start
- Go to pgadmin.pocketcode.in
- Sign in with email/password from
PGADMIN_DEFAULT_EMAIL/PGADMIN_DEFAULT_PASSWORDenv vars - Add a new server connection (first-time only)
Connect to the Shared Postgres
In pgAdmin: right-click Servers β Register β Server. Then:
| Tab | Field | Value |
|---|---|---|
| General | Name | ai-stack-db (anything) |
| Connection | Host | sim-db-1 |
| Connection | Port | 5432 |
| Connection | Maintenance DB | postgres |
| Connection | Username | From your ~/ai-stack/sim/.env (POSTGRES_USER) |
| Connection | Password | From your ~/ai-stack/sim/.env (POSTGRES_PASSWORD) |
What Databases Are There?
| Database | Used by |
|---|---|
ailab | General-purpose (you can use freely) |
simstudio | Sim.ai workflows & users |
openclaw | OpenClaw sessions |
n8n | n8n workflows & credentials |
ollama_results | If you stash Ollama outputs (optional) |
Common Tasks
Run a query:
- Navigate to a database in the tree
- Right-click β Query Tool
- Type SQL β F5 to execute
Export a table to CSV:
- Right-click a table β Import/Export Data
- Choose Export, set CSV format, browse for output location
Backup the whole stack DB:
docker exec sim-db-1 pg_dumpall -U postgres > ~/ai-stack/backups/all-$(date +%Y%m%d).sql
pgvector queries (Sim.ai uses this):
-- Find vectors closest to a target SELECT id, content, embedding <-> '[0.1, 0.2, ...]'::vector AS distance FROM documents ORDER BY distance LIMIT 5;
This Setup's Quirks
sim-db-1 container uses the pgvector/pgvector:pg17 image. CREATE EXTENSION vector; already done in each database.
Resources
What This Is
A real bash shell on your VPS, running in your browser. The prompt shows your actual server hostname (root@srv1234:~#) β not a container ID β because ttyd runs as a host systemd service, not in Docker. Full root access: you can apt install packages, run systemctl, edit any file, and of course docker ps / docker exec any container. Perfect for quick admin from your phone, tablet, or any browser, without SSH client setup.
Quick Start
- Go to terminal.pocketcode.in
- If not logged in β bounces you to
pocketcode.into log in - Terminal loads β you see
root@<your-vps-hostname>:~#prompt - Type any command and press Enter
Keyboard Shortcuts
| Shortcut | What it does |
|---|---|
Ctrl+Shift+C | Copy selected text (browsers reserve plain Ctrl+C) |
Ctrl+Shift+V | Paste from clipboard |
| Right-click | Context menu with paste option |
Ctrl+C in terminal | Interrupt running process (e.g. stop a tail -f) |
Ctrl+D | Exit current shell (reconnects automatically) |
What You Can Do
Manage every container:
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
docker logs sim-simstudio-1 --tail 50
docker restart n8n
docker exec -it ollama bash
Edit any config file (no SSH needed):
nano ~/ai-stack/caddy/Caddyfile nano ~/ai-stack/sim/.env docker exec caddy caddy reload --config /etc/caddy/Caddyfile
Install host packages (you're root, no sudo needed):
apt install -y htop btop ncdu jq tmux tree htop # live process viewer ncdu /root/ai-stack # disk usage explorer
Use stack aliases (already loaded β this is your real bash):
ai-start # bring up all containers ai-stop # graceful shutdown ai-doctor # full diagnostic ai-status # quick container health snapshot ai-logs n8n # tail any service's logs ai-reboot # restart whole stack
Pull new Ollama models:
docker exec ollama ollama pull qwen2.5:7b docker exec ollama ollama list
Run Claude Code from here:
docker exec -it claude-code bash claude "review the start-all.sh script for race conditions"
Query the shared Postgres:
docker exec -it sim-db-1 psql -U postgres -d simstudio \dt -- list tables \q -- quit
Manage the ttyd service itself:
systemctl status ttyd # is it running? systemctl restart ttyd # clears any stuck sessions journalctl -u ttyd -f # live log tail
How This Setup Works
/usr/bin/ttyd via apt, supervised by systemd (/etc/systemd/system/ttyd.service). It binds to 0.0.0.0:7681 and Caddy reaches it via host.docker.internal:7681 β a name that resolves to the host's IP on the ai-stack Docker bridge.
https://terminal.pocketcode.in.
Resources
- ttyd on GitHub β the web terminal binary
- Docker CLI reference
Architecture
One PostgreSQL container β sim-db-1 running pgvector/pgvector:pg17 β hosts every database in the stack. Sim.ai brings it up (it's in the Sim.ai compose file), but pgAdmin, n8n, OpenClaw, and any custom workflow share the same instance. The container sits on two Docker networks simultaneously: sim_default (Sim.ai's own) and ai-stack (the rest of the stack), so anyone can reach it.
The Six Databases
| Database | Owner / Primary user | What's inside |
|---|---|---|
simstudio | Sim.ai | Workflows, agents, users, runs, knowledge base embeddings (pgvector) |
n8n | n8n | Workflows, credentials (encrypted), executions, webhooks |
openclaw | OpenClaw | Sessions, chat history, device pairings |
ailab | General-purpose | Free for your own use β custom tables, prototypes, scratch |
ollama_results | Optional | If you stash inference outputs from cron jobs, n8n, etc. |
voice_pg Β· v2 | livekit-agent + voice-playground + mcp-voice-history | Operational store for the voice agent: 8 tables β callers, calls, turns, tool_invocations, events, callbacks, airtable_outbox, and kb_chunks (768-dim pgvector embeddings of the product KB). Auto-bump trigger keeps callers.total_calls and last_seen in sync. Reads served read-only via mcp-voice-history for AI clients. |
Connection Strings
| From | How to connect |
|---|---|
| Inside Docker (any service on ai-stack) | postgres://USER:PASS@sim-db-1:5432/<db> |
| pgAdmin in browser | Host = sim-db-1, Port = 5432 (Sim.ai's .env has the password) |
| Web terminal (CLI psql) | docker exec -it sim-db-1 psql -U postgres -d <db> |
pgvector Extension
Already installed in every database. Use it for semantic search inside Postgres without needing Qdrant for small datasets:
-- nomic-embed-text returns 768-dim vectors CREATE TABLE documents ( id BIGSERIAL PRIMARY KEY, content TEXT, embedding vector(768), created_at TIMESTAMPTZ DEFAULT now() ); -- Index for fast similarity search CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops); -- Find nearest neighbors SELECT id, content, embedding <=> '[0.1, 0.2, ...]'::vector AS distance FROM documents ORDER BY distance LIMIT 5;
Common Tasks
Get a psql shell:
docker exec -it sim-db-1 psql -U postgres -d ailab
List all databases:
docker exec sim-db-1 psql -U postgres -l
Backup a single database:
docker exec sim-db-1 pg_dump -U postgres -Fc -d n8n > ~/backups/n8n-$(date +%Y%m%d).pgcustom
Backup everything (pg_dumpall):
docker exec sim-db-1 pg_dumpall -U postgres > ~/backups/all-$(date +%Y%m%d).sql
Restore from backup:
cat ~/backups/all-20260516.sql | docker exec -i sim-db-1 psql -U postgres
Create a new database for your project:
-- via pgAdmin Query Tool or psql shell CREATE DATABASE my_project; \c my_project CREATE EXTENSION vector;
Connection Pool Limits
Default Postgres config allows ~100 concurrent connections. Sim.ai + n8n + OpenClaw typically use ~30. If you start running into "too many connections" errors, increase the limit:
docker exec sim-db-1 psql -U postgres -c "ALTER SYSTEM SET max_connections=200;" docker restart sim-db-1
This Setup's Quirks
docker compose down from ~/ai-stack/sim/, the database goes away. ai-stop handles this gracefully (stops dependent services first), but if you ever manually run compose commands, be aware.
sim_default AND ai-stack. After a Sim.ai recompose, it sometimes drops the ai-stack attachment β ai-start reconnects it automatically. If you see "host sim-db-1 not found" errors from n8n or OpenClaw, run docker network connect ai-stack sim-db-1 manually.
n8n-data), not in Postgres. Back up that volume separately if you care about preserving stored credentials across rebuilds.
Resources
The Network Architecture
Every container in the stack is on the ai-stack Docker network. Docker provides automatic DNS resolution between containers using their container names. Sim.ai's compose stack has its own internal network sim_default, and sim-db-1 straddles both so services on either network can reach it.
Internal Hostnames
Inside any container on ai-stack, these hostnames resolve automatically:
| Hostname | Port | What it is | Common use |
|---|---|---|---|
ollama | 11434 | Ollama API | LLM inference |
open-webui | 8080 | Open WebUI | β |
auth-gateway | 7000 | Gateway service | Caddy forward_auth |
sim-db-1 | 5432 | PostgreSQL | Any service's database |
sim-simstudio-1 | 3000 | Sim.ai web app | Caddy upstream |
sim-realtime-1 | 3002 | Sim.ai WebSocket | Workspace live collab |
sim-redis-1 | 6379 | Redis (Sim.ai) | Sim.ai queues, sessions |
openclaw | 8080 | OpenClaw | β |
n8n | 5678 | n8n | β |
openrouter-proxy | 4000 | LiteLLM proxy | Cloud model gateway |
qdrant | 6333 | Qdrant REST | Vector search |
pgadmin | 80 | pgAdmin | β |
terminal | 7681 | Web terminal (ttyd) | β |
caddy | 80, 443 | Reverse proxy | β |
Why Use Internal Hostnames?
- Faster β stays inside Docker, no DNS over the internet, no TLS handshake
- More reliable β no dependence on external DNS or Caddy being up
- No auth wall β gateway auth is at Caddy; internal calls skip it (intentional: they're already trusted within Docker)
- Container IPs change on restart, but the hostname always resolves to the current IP
Common Patterns
n8n calling Ollama: In n8n's Ollama Chat Model credentials, set Base URL to:
http://ollama:11434
n8n calling OpenRouter (cloud models) via the proxy:
Base URL: http://openrouter-proxy:4000 API Key: any-non-empty-string
Sim.ai calling Ollama: already wired via Sim.ai's OLLAMA_URL=http://ollama:11434 env var.
Custom script calling everything from a terminal:
curl http://ollama:11434/api/tags curl http://qdrant:6333/collections curl http://openrouter-proxy:4000/v1/models
Connecting from a brand-new container you create: Add it to the ai-stack network:
docker run -d --name my-app \ --network ai-stack \ -e DB_URL=postgres://postgres:PASS@sim-db-1:5432/ailab \ -e LLM_URL=http://ollama:11434 \ my-image:latest
Adding a Service to ai-stack
- Run with
--network ai-stackflag (ornetworks: [ai-stack]in compose) - Once started, it can reach every other container by name
- If it needs to be public via HTTPS, add a Caddyfile block per Tab 13
Auto-SSO Across Services
The gateway at pocketcode.in ships with optional auto-SSO: when you sign in at the apex, hidden iframes silently log you into Sim.ai, Open WebUI, and n8n using credentials stored in ~/ai-stack/auth-gateway/.env. Three things are happening:
| Phase | What runs | How it works |
|---|---|---|
| 1. Master login | Gateway sets pocketcode_session cookie scoped to .pocketcode.in | One cookie sent to every subdomain β Caddy's forward_auth sees it and lets you through |
| 2. Bootstrap | home.html creates hidden iframes pointing at https://<svc>.pocketcode.in/_sso_init | Caddy proxies /_sso_init to the gateway; gateway POSTs to the service's login API internally, forwards Set-Cookie back through Caddy β cookie scoped to <svc>.pocketcode.in |
| 3. Watchdog | iframes re-load every 2 min and on tab focus | If you got logged out of any service in the meantime, the next refresh logs you back in. No way to "stay out" of a service while the master session is alive. |
Which services auto-SSO works for
| Service | SSO? | Why / why not |
|---|---|---|
| Sim.ai | β | BetterAuth's POST /api/auth/sign-in/email accepts JSON, returns a session cookie |
| Open WebUI | β | POST /api/v1/auths/signin JSON + cookie-based auth |
| n8n | β | POST /rest/login JSON + n8n-auth session cookie |
| OpenClaw | β | Device pairing only, no credential-based login |
| pgAdmin | β | Requires CSRF token (two-step login); skipped for simplicity. Sessions persist 30+ days though, so a one-time login. |
| Ollama / OpenRouter | β | No login β gated by Caddy's auth_gate directly |
| Qdrant | β | No login β gated by auth_gate AND has a session-poll script injected into its HTML via Caddy's replace-response plugin (v1.6). See docs-page Qdrant tab for the full access pattern. |
The /_sso_init endpoint
Each SSO-enabled subdomain has this Caddyfile block:
sim.pocketcode.in {
import auth_gate
handle /_sso_init {
reverse_proxy auth-gateway:7000 {
rewrite /sso-init/sim
}
}
reverse_proxy sim-simstudio-1:3000
}
Browser loads /_sso_init in a hidden iframe β Caddy proxies to gateway β gateway logs into Sim.ai server-side β response carries Set-Cookie for the user β iframe posts a status message back to home.html.
What logging out does
- Logout at pocketcode.in: only the gateway cookie is cleared. Service-internal cookies remain valid. But Caddy now sees no gateway cookie β every
*.pocketcode.inrequest gets 302'd to the login page. After re-login, every service is instantly accessible (their sessions never died). - Logout inside a service (e.g., clicking Sim.ai's own "Logout" button): that service's session cookie is cleared. The next watchdog tick at pocketcode.in (β€ 2 min) re-runs
/_sso_initfor that service and restores the session. - Closing the pocketcode tab: Watchdog stops. Sessions naturally expire after their normal TTL. Re-opening pocketcode restarts the watchdog.
Troubleshooting
"Host not found" / "getaddrinfo ENOTFOUND":
- Verify the container is on
ai-stack:docker inspect <name> --format='{{json .NetworkSettings.Networks}}' - If missing, attach it:
docker network connect ai-stack <name> - For sim-db-1 specifically,
ai-startauto-reconnects it after Sim.ai recompose
"Connection refused" but container is up:
- The service may not be listening on
0.0.0.0internally. Inspect:docker exec <name> netstat -tlnp - Or it's still booting.
docker logs <name> --tail 20
n8n shows "Failed to load model catalog" briefly at start:
- Transient Docker DNS race after a recompose.
ai-doctorfilters it. Self-heals within ~10 seconds.
Resources
- Docker networking
- See Tab 9 (Inter-Service Comms) in the setup guide for initial wiring
Recipe 1: Slack-to-Ollama Chatbot (via n8n)
Listen for Slack messages, route through Ollama, respond.
- In n8n: Slack Trigger β AI Agent (Ollama, model llama3.2:3b) β Slack Send Message
- Use the message text as user input to the agent
- Pass the agent output back to Slack channel
Recipe 2: Document Q&A System (Open WebUI + Ollama)
- Pull embedding model:
docker exec ollama ollama pull nomic-embed-text - In Open WebUI: Settings β Documents β Embedding Model =
nomic-embed-text - Workspace β Knowledge β New Collection β upload PDFs
- In chat, reference with
#collection-name
Recipe 3: Scheduled Web Scraping (n8n + OpenRouter)
- n8n: Schedule trigger (daily 9am) β HTTP Request (fetch URL) β AI Agent (summarize via OpenRouter Claude) β Send Email/Slack
- Claude does the summarization since it's better at structured output
- Costs ~$0.01-0.05 per run depending on page size
Recipe 4: Custom RAG Pipeline (Ollama + Qdrant + Sim.ai)
- Create a Qdrant collection (size 768 for nomic-embed-text)
- In Sim.ai: build a workflow that takes a query, embeds via Ollama, searches Qdrant, passes top-K results + query to an Agent block
- Agent uses retrieved context + query to produce grounded answer
Recipe 5: Multi-Model Comparison (LiteLLM)
Compare outputs from local Ollama vs cloud Claude vs GPT side-by-side.
PROMPT="Explain quantum computing in 2 sentences"
# Local Ollama
curl -s http://localhost:11434/api/generate \
-d "{\"model\":\"llama3.2:3b\",\"prompt\":\"$PROMPT\",\"stream\":false}" \
| jq -r .response
# Cloud Claude via OpenRouter proxy
curl -s http://localhost:4000/v1/chat/completions \
-H "Content-Type: application/json" \
-d "{\"model\":\"claude-sonnet-4\",\"messages\":[{\"role\":\"user\",\"content\":\"$PROMPT\"}]}" \
| jq -r '.choices[0].message.content'
Recipe 6: Voice Pipeline (Open WebUI + Whisper)
Open WebUI supports voice input via Whisper. To enable:
- Settings β Audio β STT Engine: select "Local Whisper"
- Pick a Whisper model size (base / small / medium)
- Click π€ icon in chat input β speak β transcribed and sent
Recipe 7: Backup & Restore the Stack
Backup (run daily as a cron job or n8n workflow):
#!/bin/bash DATE=$(date +%Y%m%d-%H%M) mkdir -p ~/backups/$DATE # Postgres dump docker exec sim-db-1 pg_dumpall -U postgres > ~/backups/$DATE/postgres.sql # Critical volumes (n8n encryption key, Open WebUI chats, etc.) docker run --rm -v n8n-data:/data -v ~/backups/$DATE:/backup \ alpine tar czf /backup/n8n-data.tar.gz -C /data . docker run --rm -v open-webui-data:/data -v ~/backups/$DATE:/backup \ alpine tar czf /backup/open-webui-data.tar.gz -C /data . # Caddy data (certs) docker run --rm -v caddy_data:/data -v ~/backups/$DATE:/backup \ alpine tar czf /backup/caddy_data.tar.gz -C /data . # Compress everything tar czf ~/backups/$DATE.tar.gz -C ~/backups $DATE rm -rf ~/backups/$DATE
Recipe 8: Manage Stack from the Web Terminal
Everything in this guide can be done from terminal.pocketcode.in in your browser:
ai-start # bring up all containers ai-stop # shut down cleanly ai-doctor # diagnostic scan ai-logs SERVICE # tail any service's logs ai-status # quick health check
docker ps, docker exec, docker logs all work. Edit any config file under ~/pocketcode-project/.
The PocketCodeIn stack runs HTTP MCP servers behind Caddy on mcp-*.pocketcode.in subdomains. They are NOT browser-interactive β they're API endpoints consumed by LLM clients. Every MCP uses the same authentication: Authorization: Bearer <MCP_BEARER_TOKEN>, where the token lives in ~/pocketcode-project/secrets/secrets.json under mcp.bearer_token.
mcp-postgres
URL: https://mcp-postgres.pocketcode.in/mcp
Source: ~/pocketcode-project/services/mcp-postgres/
Connects as: postgres superuser β sim-db-1 (5 databases)
Tools exposed
| Tool | Purpose |
|---|---|
list_databases() | Returns the 5 db names: simstudio, n8n, openclaw, ailab, ollama_results |
list_tables(database) | Returns [{schema, table}, β¦] across all non-system schemas |
describe_table(database, table, schema='public') | Returns [{column, type, nullable, default}, β¦] |
query(sql, database='simstudio') | Executes SELECT/INSERT/UPDATE/DELETE/DDL. Capped at 1000 rows. 30-second statement timeout. |
Using from Claude Code (on the VPS)
Already wired up via ~/pocketcode-project/.mcp.json. To launch a Claude Code session that picks it up:
tmux new -s claude cd ~/pocketcode-project source secrets/secrets.env # exports MCP_BEARER_TOKEN claude # reads .mcp.json, connects to mcp-postgres over HTTPS
In the Claude Code session, the MCP tools show up under /mcp. Ask Claude things like "list all tables in the n8n database" or "how many workflows are saved in sim?"
Using from Claude Desktop (on your Mac)
Add to your Claude Desktop MCP config (~/Library/Application Support/Claude/claude_desktop_config.json on macOS):
{
"mcpServers": {
"pocketcode-postgres": {
"type": "http",
"url": "https://mcp-postgres.pocketcode.in/mcp",
"headers": {
"Authorization": "Bearer <PASTE MCP_BEARER_TOKEN HERE>"
}
}
}
}
Get the token value with jq -r .mcp.bearer_token ~/pocketcode-project/secrets/secrets.json on the VPS, then paste into the Mac config. Restart Claude Desktop.
Quick verification (manual)
TOKEN="<your MCP_BEARER_TOKEN>"
# Health check β no auth needed
curl https://mcp-postgres.pocketcode.in/health
# β OK
# List databases β proper MCP handshake then tool call
INIT=$(curl -s -D /tmp/h https://mcp-postgres.pocketcode.in/mcp \
-X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"clientInfo":{"name":"curl","version":"0"}}}')
SESSION=$(grep -i '^mcp-session-id:' /tmp/h | awk '{print $2}' | tr -d '\r')
curl -s -X POST https://mcp-postgres.pocketcode.in/mcp \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "mcp-session-id: $SESSION" \
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
curl -s -X POST https://mcp-postgres.pocketcode.in/mcp \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "mcp-session-id: $SESSION" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_databases","arguments":{}}}'
postgres superuser (full DDL). Any prompt-injection attack on a connected LLM could in principle execute destructive SQL. This is a deliberate project trade-off (consistency with v1.0 services) β see CLAUDE.md. If you ever expose this MCP to less-trusted callers, add a read-only role and switch the URL.
mcp-airtable
URL: https://mcp-airtable.pocketcode.in/mcp
Source: ~/pocketcode-project/services/mcp-airtable/
Default base: the call_logs base from secrets.json β airtable.base_call_logs. Override per-tool with the base_id argument.
Tools (full CRUD)
| Tool | Purpose |
|---|---|
list_bases() | All bases the PAT can access |
list_tables(base_id='') | Tables + field schemas in a base |
describe_table(table, base_id='') | Schema of one table by ID or name |
list_records(table, base_id='', view='', max_records=100, filter_formula='') | Records with optional view + filter; cap 1000 |
get_record(table, record_id, base_id='') | Single record by ID |
create_record(table, fields, base_id='') | Insert; typecast=true so strings auto-coerce |
update_record(table, record_id, fields, base_id='') | PATCH β only listed fields change |
Example: log a call from the voice agent (Phase 4 use case)
create_record(
table="Call Logs",
fields={
"caller_id": "+15551234567",
"timestamp": "2026-05-20T11:23:45Z",
"duration_sec": 142,
"transcript": "Caller: Hi, I need help with...",
"summary": "Customer asked about pricing tier; agent quoted KVM 4 plan."
}
)
mcp-web-search
URL: https://mcp-web-search.pocketcode.in/mcp
Source: ~/pocketcode-project/services/mcp-web-search/
Backend: Brave Search API. Free tier 2000 queries/month. Check quota at api.search.brave.com/app/dashboard.
Tool
| Param | Type | Notes |
|---|---|---|
query | str (required) | The search terms |
count | int = 10 | 1-20 (Brave per-page max). Defaults to 10. |
freshness | str = '' | Empty = any time. Or day / week / month / year |
country | str = '' | ISO-2 like IN, US, GB. Empty = Brave's auto-detect. |
safesearch | str = 'moderate' | off / moderate / strict |
Returns {query, total_results, results: [{title, url, description, age, language, is_source_local}]}. On rate-limit (429), returns {error: 'Brave Search rate limit hit', hint: ...} so the LLM can react gracefully.
Example queries
web_search(query="latest news on Anthropic") web_search(query="Hostinger KVM 8 pricing", country="IN") web_search(query="MCP protocol spec", freshness="month", count=20) web_search(query="open source TTS comparison", safesearch="strict")
mcp-sim
URL: https://mcp-sim.pocketcode.in/mcp
Source: ~/pocketcode-project/services/mcp-sim/
What it is: the architectural replacement for the dropped mcp-n8n. Wraps sim.ai's REST API for workflow CRUD + execution β the flagship "Claude Code builds workflows" use case.
Tools (9)
| Tool | Purpose |
|---|---|
list_workspaces() | Workspaces the operator can access (start here) |
list_workflows(workspace_id='', scope='') | Workflows, optionally filtered by workspace or scope |
get_workflow(workflow_id) | Full payload (definition + metadata) |
get_workflow_state(workflow_id) | Live state snapshot (nodes, edges, runtime) |
create_workflow(name, workspace_id, color?, folder_id?, description?) | New (empty) workflow β fill in nodes/edges via UI or future state endpoint |
update_workflow(workflow_id, name?, color?, description?, folder_id?) | PATCH metadata |
delete_workflow(workflow_id) | Remove |
execute_workflow(workflow_id, input?) | Trigger run; returns result or execution_id |
get_workflow_logs(workflow_id, limit=20) | Recent execution logs |
Auth β why service-account login (not API key)
Sim has two auth paths: personal API keys and BetterAuth session cookies. API keys are restricted to per-workflow execution endpoints β they cannot list, create, update, or delete workflows. CRUD requires a real user session. So mcp-sim uses session cookies via auto-login with the operator's credentials (same ones auth-gateway uses for browser SSO). 30-day session TTL; auto-refreshes on 401. SIM_SSO_EMAIL + SIM_SSO_PASSWORD come from secrets.json via regenerate-env.sh.
Example flow β "Claude Code builds a workflow"
# 1. Discover available workspaces
list_workspaces()
# β {workspaces: [{id: "4a5acb0a...", name: "Nishant's Workspace", role: "owner"}]}
# 2. List what's already there
list_workflows(workspace_id="4a5acb0a...")
# β {workflows: [...]}
# 3. Create a new workflow shell
create_workflow(name="YouTube Summarizer", workspace_id="4a5acb0a...",
description="Take a YouTube URL, return a one-paragraph summary")
# β {id: "wf_xyz...", name: "YouTube Summarizer", ...}
# 4. (Use sim's UI or the state endpoint to populate nodes/edges β
# this is the part that's still better done visually for now)
# 5. Execute it
execute_workflow(workflow_id="wf_xyz...", input={"url": "https://youtube.com/watch?v=..."})
# β {success: true, output: "...", execution_id: "..."}
# 6. Inspect logs
get_workflow_logs(workflow_id="wf_xyz...")
create_workflow creates an EMPTY workflow. Populating nodes/edges programmatically still requires the /api/workflows/{id}/state endpoint, which this MCP doesn't yet wrap. For now the pattern is: Claude creates the shell + describes the workflow β you fill nodes visually in sim's UI. A future iteration could add a set_workflow_state tool to close that gap.
mcp-n8n
URL: https://mcp-n8n.pocketcode.in/mcp
Source: ~/pocketcode-project/services/mcp-n8n/
What it is: companion to mcp-sim β wraps n8n's public REST API for workflow CRUD + execution. Lives alongside mcp-sim; pick whichever workflow engine fits the task.
Tools (12)
| Tool | Purpose |
|---|---|
list_workflows(active?, tags?, limit=100) | Filtered list |
get_workflow(workflow_id) | Full nodes/connections/settings |
create_workflow(name, nodes?, connections?, settings?) | New (minimum: just name) |
update_workflow(workflow_id, ...) | PUT-semantics; fetches current, merges, sends |
delete_workflow(workflow_id) | Remove |
activate_workflow(workflow_id) | Enable triggers |
deactivate_workflow(workflow_id) | Disable triggers |
execute_workflow(workflow_id, input?) | One-shot run (fallback guidance if API version doesn't support) |
list_executions(workflow_id?, status?, limit=50) | Recent runs |
get_execution(execution_id, include_data=false) | Single run; include_data for per-node I/O |
list_credentials() | Schema metadata (no values) |
list_tags() | Workflow tags |
Auth
Uses n8n's public API key (header X-N8N-API-KEY). Minted programmatically via n8n's /rest/api-keys endpoint with all 68 scopes granted. Stored at secrets.json β services.n8n.api_key, exported as N8N_API_KEY by regenerate-env.sh.
yt-dlp-ui (browser UI for downloads/transcripts)
URL: https://ytdl.pocketcode.in (master-login gated)
Source: ~/pocketcode-project/services/yt-dlp-ui/
What it is: a 3-tab single-page web app for using yt-dlp from the browser β with real-time progress bars on downloads. Companion to mcp-yt-dlp (which serves LLM agents). Same dependencies + shared data/downloads/ directory.
Three tabs
- π Paste URL β paste any yt-dlp-supported URL, click "Get info" to fetch metadata (title, channel, duration, views, thumbnail). Then options panel: resolution dropdown (240β2160 or "max"), audio format/quality dropdowns, optional trim (HH:MM:SS start/end). Three action buttons: Download Video, Extract Audio, Get Transcript.
- π Search YouTube β search query + limit β grid of result cards (title, channel, duration, view count). Clicking a card loads it into the URL tab.
- π Recent files β lists everything in
data/downloads/with one-click "Download" buttons that stream the file to the user's computer.
Real-time progress
When you click Download Video or Extract Audio, yt-dlp-ui runs yt-dlp directly as a subprocess with --newline --progress-template. Each progress line is parsed and emitted as a Server-Sent Event to the browser. The on-screen progress bar fills in real time with downloaded/total MB, current speed, and ETA. Post-processing stages (ExtractAudio, Merger) are shown as status text. On completion, you see a "Done" indicator with a direct "Download to your computer" link.
How it relates to mcp-yt-dlp
| Aspect | mcp-yt-dlp | yt-dlp-ui |
|---|---|---|
| Audience | LLM agents (Claude Code, sim, voice) | Humans in a browser |
| Transport | HTTP+SSE MCP protocol | HTTP (FastAPI) + SSE for downloads |
| Auth | Bearer token (shared MCP token) | Master JWT cookie via Caddy auth_gate |
| Subdomain | mcp-yt-dlp.pocketcode.in | ytdl.pocketcode.in |
| Downloads dir | /downloads (host: data/downloads/) | Same β shared bind-mount |
| Cookies.txt | Auto-detected at /downloads/cookies.txt | Same β shared |
Calling yt-dlp-ui for search/metadata internally proxies to mcp-yt-dlp via HTTP+bearer, so there's one canonical implementation of those tools. Downloads run yt-dlp directly in yt-dlp-ui because real-time progress streaming needs subprocess management that doesn't fit MCP's request/response shape.
secrets.env server-side and uses it only when calling mcp-yt-dlp. The browser sees only the cookie-authenticated UI surface (/api/search, /api/download/*, etc.).
mcp-yt-dlp
URL: https://mcp-yt-dlp.pocketcode.in/mcp
Source: ~/pocketcode-project/services/mcp-yt-dlp/
Design lineage: tool surface inspired by kevinwatt/yt-dlp-mcp (MIT) β reimplemented natively in Python to match this project's existing FastMCP + subprocess pattern. No Node.js / Deno runtime needed.
Tools (8)
| Tool | Purpose |
|---|---|
ytdlp_search_videos(query, limit=10) | YouTube search; capped at 50 results |
ytdlp_list_subtitle_languages(url) | Manual + auto-generated subtitle language codes available |
ytdlp_download_video_subtitles(url, language='en') | VTT subtitle text β full content returned |
ytdlp_download_transcript(url, language='en') | Plain text transcript (timing stripped) β fastest "what's said" extraction |
ytdlp_download_video(url, resolution='720', start='', end='') | Download video file; optional time-range trim. Resolutions: 240/360/480/720/1080/1440/2160/max |
ytdlp_download_audio(url, format='mp3', quality='192') | Extract audio. format: mp3/m4a/opus/wav/flac. quality: 128/192/256/320 kbps |
ytdlp_get_video_metadata(url) | Full yt-dlp JSON metadata (id, title, formats, thumbnails, etc.) |
ytdlp_get_video_metadata_summary(url) | Compact LLM-friendly summary (title, channel, duration, view count, description preview) |
Downloads location
Files written by download_video and download_audio land in ~/pocketcode-project/data/downloads/ on the host. The tool response includes both container path (/downloads/<file>) and host path so other services can reference them.
Fix: export a YouTube cookies.txt from your logged-in browser (use the "Get cookies.txt LOCALLY" extension), then:
scp ~/Downloads/cookies.txt root@VPS:/root/pocketcode-project/data/downloads/The MCP picks it up automatically on the next call. No container restart needed.
Example tool calls
ytdlp_search_videos(query="livekit voice agent tutorial", limit=5)
ytdlp_get_video_metadata_summary(url="https://youtube.com/watch?v=...")
# β {title, channel, duration_str, view_count, description_preview, ...}
ytdlp_download_transcript(url="https://youtube.com/watch?v=...", language="en")
# β {url, language, text} (timing-stripped, ready to summarize)
ytdlp_download_audio(url="...", format="mp3", quality="192")
# β file lands at ~/pocketcode-project/data/downloads/<id>_<title>.mp3
voice-playground β browser harness + outbound dialer
URL: https://voice.pocketcode.in (master-login gated)
Source: ~/pocketcode-project/services/voice-playground/
What it is: a single-page app with two tabs β Audio test with agent (talk to Adrian from your browser) and Outbound AI call (dial a real phone, listen + watch live transcript, schedule callbacks, view call history).
Architecture (end-to-end)
The diagram below traces every path the playground supports β browser-only test calls (left side), outbound PSTN dial-outs (middle), the agent's runtime pipeline (LLM/STT/TTS + tools), and the operational data plane (voice_pg + Qdrant + Airtable outbox + scheduled-callback auto-dialer).
Flow narrative
- Audio Test path. Browser POSTs
/api/tokenβ voice-playground mints a LiveKit token, pre-creates the room with metadata (llm,tts,agent_name,agent_gender,product_kb="audio_test_mode.md") β browser joins room and publishes mic β the livekit-agent worker auto-dispatches into the room β STTβLLMβTTS pipeline β audio streams back to the browser. - Outbound path. Browser POSTs
/api/dialwithto_number, optionalsupervisor_number, prompt, model, voice β voice-playground calls LiveKit'sCreateSIPParticipantwithwait_until_answered=Trueβ LiveKit SIP gateway dials Twilio β Twilio rings the destination β on pickup, the SIP participant joins the room and the agent joins too. The same call returns an observer LiveKit token the browser uses to listen in + render the WhatsApp-style live transcript. - Per-turn pipeline (preemptive). Deepgram emits interim transcripts β LLM starts generating BEFORE end-of-turn confirmation (saves ~300β600 ms first-token) β tokens stream into the TTS plugin β audio frames flow to LiveKit and out to the caller. The ML turn detector decides when the user really IS done, instead of relying on pure VAD silence.
- Tools at runtime. The LLM may call
kb_search(embed via Ollama nomic-embed-text β cosine search overvoice_pg.kb_chunks),caller_memory(Qdrantvoice_callsfiltered by caller_number),web_search(Brave Search via mcp-web-search),lookup_caller(mcp-postgres),transfer_to_supervisor(CreateSIPParticipant for the supervisor number, with hold music + 3-attempt retry),schedule_callback(INSERT intovoice_pg.callbackswithscheduled_for, validated against the 48-hour window), orhang_up(DeleteRoom). - End-of-call writes. Agent finalises the
callsrow invoice_pg(outcome, duration, summary), pushes the summary vector into Qdrant for future-caller lookup, and enqueues an Airtable write invoice_pg.airtable_outbox. The outbox worker drains pending rows to Airtable every 8 s β Airtable outage no longer drops calls. - Auto-dialed callbacks. The scheduled-callback worker polls
voice_pg.callbacksevery 30 s with a 60 s look-ahead. Due rows trigger anotherCreateSIPParticipantβ LiveKit β Twilio β PSTN. Failed dials pushscheduled_forback 5 minutes. The UI's Pending callbacks card surfaces each row with scheduled time + buttons to β Cancel or β Mark done.
Architecture
Browser
β (master-login JWT via auth_gate)
βΌ
voice-playground (FastAPI, this service)
βββ GET / static index.html + livekit-client SDK
βββ POST /api/token mints a short-lived LiveKit access token
(api_secret stays server-side)
β
βΌ (browser uses the token to join)
LiveKit Cloud (Germany 2 region)
β
βΌ (Dispatch Rule SDR_xxts7WxxddQj β same one used for PSTN)
livekit-agent β Silero VAD β Deepgram STT β Claude β ElevenLabs TTS
UI features
- Big call button β tap to start, tap again to hang up. Visual pulsing while connected.
- Status pill color-coded for idle / connecting / connected / agent-present / error.
- Dual VU meters β your mic level (Web Audio API analyser on the local track) + the agent's voice level (MediaElementSource analyser on the playback element).
- Conversation log β system events + transcripts if the agent publishes them via LiveKit's data channel.
- Info chips β room name, your identity, agent identity, live call duration.
- Advanced (collapsed) β custom room/identity overrides for testing scenarios where you need to re-join a specific session.
vs the PSTN path
| Aspect | PSTN (call +1 534-231-0196) | voice-playground |
|---|---|---|
| Ingress | Twilio DID β SIP trunk β LiveKit SIP gateway | Browser β LiveKit WebRTC (same project) |
| Cost | ~$0.013/min inbound Twilio | Free (LiveKit free tier 50 GB egress/mo) |
| Latency | +SIP processing overhead | Just WebRTC RTT to LiveKit Germany 2 |
| Pipeline | Same agent, same STT/LLM/TTS | Same agent, same STT/LLM/TTS |
| Use case | Production / real callers | Dev, testing, demos |
SDR_xxts7WxxddQj) auto-dispatches the registered livekit-agent worker into any new room β whether the room was created by an inbound SIP call or by the playground's token-minting flow. One agent, two ingress paths.
livekit-agent β Adrian, the voice agent
NOT a browser-visible service. Python worker that registers with LiveKit Cloud and waits for dispatched calls. Source: ~/pocketcode-project/services/livekit-agent/.
Persona
The agent is Adrian, a representative from Pocket Code dot IN. Greets every call with "Hi, this is Adrian, a representative from Pocket Code dot IN, and this is a test call. How are you doing today?" Never reveals it's an AI. Configurable via AGENT_NAME / AGENT_ROLE / AGENT_GREETING env vars. Listens for the caller calling it by name ("Adrian") as an attention signal.
Persona name + gender pronouns follow the selected voice. Pick Orpheus (male) β agent introduces as Orpheus, refers to itself with he/him; Stella (female) β Stella, she/her; neutral β they/them. Wired by the voice-playground server from each voice's catalog entry into the room metadata, then injected as a "Self-reference" block into the system prompt at call time. The chosen agent_gender is also persisted on voice_pg.calls.agent_gender for analytics.
Supervisor number is optional. With one set, transfer_to_supervisor() dials with hold music + 3-attempt retry. Without one, it apologizes, promises a 24-hour callback (logged in voice_pg.callbacks with scheduled_for = now() + 24h), and hangs up cleanly.
Pipeline
Twilio DID (+1 534-231-0196)
β SIP trunk
βΌ
LiveKit Cloud SIP gateway
β WebRTC room (one per call)
βΌ
livekit-agent container
βββ VAD : Silero (in-process ONNX, tuned for phone audio)
βββ STT : Deepgram Nova-3 (streaming, en, endpointing=200ms)
βββ LLM : Anthropic Claude Haiku 4.5 (streaming; Sonnet via env override)
βββ TTS : Deepgram Aura aura-2-orpheus-en (young male, English)
β β OR ElevenLabs with a cloned voice id (USE_ELEVENLABS_TTS=1)
βββ Tools : mcp-postgres.query (caller lookup)
mcp-web-search.web_search (mid-call queries)
hang_up (LLM calls when caller says bye β deletes room)
mcp-airtable.create_record (end-of-call β call_logs table)
Latency knobs (all streamable end-to-end):
min_endpointing_delay=0.4s, max_endpointing_delay=2.0s, allow_interruptions=True
barge-in: caller talking over Adrian instantly cuts his TTS
Tools available to Claude during the call (14 function tools)
As of Phase 5.7q the agent exposes 14 function tools. The LLM calls them as needed mid-conversation; results stream back into the next turn.
| Tool | What it does |
|---|---|
lookup_caller(phone) | Looks up the caller's profile + history in voice_pg (via mcp-postgres) for returning callers. |
web_search(query) | Top Brave Search results (via mcp-web-search), returned as compact text. |
kb_search(query) | pgvector cosine search over voice_pg.kb_chunks β top-4 product-KB chunks only. |
caller_memory(query) | Qdrant voice_calls lookup filtered by caller_number β cross-call memory. |
transfer_to_supervisor(reason) | Dials the supervisor number (hold music + 3-attempt retry); falls back to a 24-hour callback if none is set. |
schedule_callback(when, reason) | INSERTs into voice_pg.callbacks (validated against the look-ahead window) for the auto-dialer to pick up. |
note_signal(kind, value) | Records a structured signal (interest, objection, budget, β¦) into voice_pg.signals. |
set_qualification(status) | Marks the lead's qualification state on the call row. |
send_followup_email(to, subject, body) | Sends a branded follow-up via mcp-gmail's send_email. |
find_demo_slots(...) | Asks mcp-gcal for 2-3 open business-hours slots. |
book_demo(slot, attendee) | Creates a Google Calendar event with an auto-Meet link via mcp-gcal. |
disqualify_lead(reason) | Records that the caller isn't a fit and closes the qualification loop. |
get_current_time() | Returns the current time in the caller's timezone β anchors all scheduling. |
hang_up(reason) | The LLM calls this when the caller signals they're done. Adrian says a brief farewell, then the LiveKit room is deleted 1.5 s later (audio flush time). |
- Canonical 10-section brain (5.7q). The agent's system prompt was rewritten from an ad-hoc blob into a fixed 10-section structure (identity, persona, knowledge, qualification, tools, scheduling, objection handling, compliance, closing, self-reference). Easier to reason about and to edit one behaviour without disturbing the rest.
- LLM failover chain (5.7p). A
Claude β OpenRouter β Ollamafallback prevents stalls when a provider is overloaded β if the primary returns an overload/5xx the worker transparently retries on the next provider so the caller never hits dead air. - Branded HTML emails (5.7m).
send_followup_emailnow sends a branded HTML template (not plain text) with Home / Setup / Docs links in the footer.
End-of-call logging (Airtable call_logs)
On any disconnect, the agent writes one row. The write is wrapped in try/except so call teardown cannot crash on an Airtable failure.
caller_numberβ E.164 phone (e.g.+15551234567)created_atβ UTC ISO timestamp of call startduration_secondsβ seconds between connect and disconnecttranscriptβ full STT transcript (capped at 8000 chars)
Cost protection
CALL_MAX_DURATION_SEC (default 600 s) starts a watchdog at call connect. If the call runs past the limit (stuck conversation, looping LLM, dead audio), the watchdog forcibly deletes the room. Set to 0 to disable.
What's needed before a real call works
The agent registers as a LiveKit worker on startup. Both PSTN directions are configured via API (no dashboard clicking) β see the project CHANGELOG for the exact recipes. High-level:
- Twilio β buy a DID, create an Elastic SIP Trunk, add an Origination URL pointing at the LiveKit SIP gateway, associate the DID. For outbound, set a Termination Domain + attach a Credential List for SIP digest auth.
- LiveKit Cloud β create an Inbound Trunk + a Dispatch Rule (auto-creates rooms with prefix
call-). For outbound, create an Outbound Trunk pointing at the Twilio termination domain with those creds. - Airtable β create a base +
call_logstable with the four fields above. Mint a PAT withdata.records:read,data.records:write,schema.bases:read. Store the base ID undersecrets.json β airtable.base_call_logs.
Testing without a real phone
Open voice.pocketcode.in. Click the π button to talk to Adrian directly from the browser (LiveKit mints a publisher token, browser publishes mic, agent auto-joins). Or use the dial-out card to call any phone β the browser joins the same room as an observer so you can listen + watch live transcripts.
Logs from the running worker
docker logs livekit-agent --tail 30 -f
agent.py monolith, its function tools delegate to sim workflows, fronted by a thin ~400-LOC worker. Same personas / LLM-TTS pipeline, different tool plumbing. Use VP1 (this page) for the canonical agent; VP2 when you want tools editable as visual sim flows.
mcp-voice β outbound PSTN calling
URL: https://mcp-voice.pocketcode.in/mcp
Source: ~/pocketcode-project/services/mcp-voice/
Auth: Authorization: Bearer ${MCP_BEARER_TOKEN}
Thin MCP wrapper over LiveKit's CreateSIPParticipant Twirp API. Lets any agent (or the dial-out card on voice-playground) place outbound calls to a real phone number. The voice agent worker auto-joins the room and talks to the callee.
Tools
| Tool | Arguments | Notes |
|---|---|---|
dial_phone | to_number (E.164), prompt?, agent_name?, room_name? | Creates the room, dials. Prompt is forwarded as a participant attribute so the agent can read it on join. |
list_active_calls | β | Lists LiveKit rooms hosting a phone-* participant. |
end_call | room_name | Deletes the LiveKit room (hangs up both legs). |
How outbound is wired (one-time API setup)
- Twilio Elastic SIP Trunk has a termination domain (e.g.
pocketcode-livekit.pstn.twilio.com) and a Credential List with a generated SIP digest user/password. - LiveKit Outbound Trunk
ST_xxxxxxxxpoints at that termination domain with those creds; allowed caller-ID is the Twilio DID we own. dial_phoneuseswait_until_answered=Trueβ the agent only joins the room once the callee picks up, so the greeting doesn't play into ringing dead air.
Example dial from curl
TOKEN=$(jq -r .mcp.bearer_token ~/pocketcode-project/secrets/secrets.json)
# (initialize + notifications/initialized first β see services/mcp-voice/README.md)
curl -sk -X POST https://mcp-voice.pocketcode.in/mcp \
-H "Authorization: Bearer $TOKEN" -H "Mcp-Session-Id: $SID" \
-H "Accept: application/json, text/event-stream" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"dial_phone","arguments":{"to_number":"+15551234567","prompt":"Confirm Tuesday 3pm dentist."}}}'
mcp-voice-history β read-only call history MCP (8th)
URL: https://mcp-voice-history.pocketcode.in/mcp
Source: ~/pocketcode-project/services/mcp-voice-history/
Auth: Authorization: Bearer ${MCP_BEARER_TOKEN}
Read-only HTTP MCP over the voice agent's operational store on the existing sim-db-1 Postgres (database voice_pg). Every call writes through here, so this MCP is the canonical entry point for any "show me what happened" question from any AI client.
Tools
| Tool | Arguments | Returns |
|---|---|---|
list_callers | limit=50 | Recent unique callers, most-recently-seen first. |
get_caller | caller_number | One caller's profile + their 10 most-recent calls. |
list_calls | limit=20, caller_number?, outcome? | Recent calls. Optional filters. |
get_call | call_id | Full detail: row + all turns + all tool_invocations. |
search_calls_by_summary | query, limit=10 | ILIKE search over summaries (pgvector per-call search wiring is a follow-up). |
list_callbacks | status='pending' | Callbacks driven by failed supervisor transfers. |
resolve_callback | callback_id, status, notes? | Mark a callback done/cancelled after dial-back. |
Operational data plane (voice_pg + Qdrant)
The agent and the voice-playground share a Postgres database voice_pg with 8 tables β callers, calls, turns, tool_invocations, events, callbacks, airtable_outbox, kb_chunks (pgvector). Plus a Qdrant collection voice_calls for cross-call memory. None of this is a new container β it's all on the existing sim-db-1 and Qdrant instances.
- pgvector KB RAG. The product KB markdown is chunked + embedded (Ollama
nomic-embed-text, 768 dims) intokb_chunks. Agent'skb_searchtool retrieves only the top-4 relevant chunks per question β full KB doesn't ride in the prompt. - Qdrant cross-call memory. End-of-call summary is embedded + upserted with caller_number as filterable payload. Agent's
caller_memorytool surfaces past calls for returning callers. - Airtable outbox. Agent writes to
airtable_outboxsynchronously; the voice-playground background worker (_outbox_loop, 8 s) drains it to Airtable's Call Logs table. Airtable outage no longer drops calls. - Pending callbacks. When a supervisor transfer fails after 3 attempts, a row lands in
callbacks. Surfaces in voice.pocketcode.in's "β° Pending callbacks" card with a π² Dial-back button.
mcp-gmail β Gmail send / draft / read / search / labels (Phase 4, 9th MCP)
URL: https://mcp-gmail.pocketcode.in/mcp
Source: ~/pocketcode-project/services/mcp-gmail/
Auth: Authorization: Bearer ${MCP_BEARER_TOKEN}
HTTP+Bearer MCP wrapping Google's Gmail API. Five tools so your AI helpers β and the voice agent's send_followup_email tool β can write emails on your behalf. Single Google Cloud Console OAuth Desktop client covers both Gmail and Calendar APIs; the refresh token lives at secrets.json.services.google.oauth_refresh_token (single source of truth after the Phase 6b consolidation).
Tools
| Tool | Arguments | Returns |
|---|---|---|
send_email | to, subject, body, cc?, bcc?, body_html? | Outbound email via your Gmail account. |
draft_email | Same as send_email | Saves to Drafts only. |
read_email | message_id | One message's content + metadata. |
search_emails | query, max_results? | Gmail-search-syntax lookup (e.g. from:nishu after:2026/05/01). |
list_labels | β | System + user labels. Good as a smoke test. |
mcp-gcal β Calendar availability + Meet booking (Phase 4, 10th MCP)
URL: https://mcp-gcal.pocketcode.in/mcp
Source: ~/pocketcode-project/services/mcp-gcal/
Auth: Authorization: Bearer ${MCP_BEARER_TOKEN}
HTTP+Bearer MCP wrapping Google's Calendar API. Seven tools for availability checks, slot finding, and event creation with auto-generated Google Meet links. Shares the OAuth refresh token with mcp-gmail β one Desktop client covers both APIs. The voice agent's find_demo_slots + book_demo tools call this MCP.
Tools
| Tool | Arguments | Returns |
|---|---|---|
list_calendars | β | All calendars you can read/write. Primary + shared. |
check_availability | start_iso, end_iso | Busy/free spans in the window. |
find_free_slots | duration_minutes, days_ahead, prefer_afternoon | 2-3 ISO datetimes of open slots (business-hours filter). |
create_event | summary, start_iso, duration_minutes, attendees[], add_meet_link, description? | New event with auto-Meet link + invites sent. |
list_events | start_iso, end_iso | Events in the window. |
get_event | event_id | Full event detail. |
delete_event | event_id | Cancels the event + notifies attendees. |
mcp-hostinger β manage the VPS / DNS / domains / billing (11th MCP)
URL: https://mcp-hostinger.pocketcode.in/ (note: the MCP endpoint is the root path /, NOT /mcp like every other server)
Source: ~/pocketcode-project/services/mcp-hostinger/
Auth: Authorization: Bearer ${MCP_BEARER_TOKEN}
Wraps Hostinger's official API MCP (hostinger-api-mcp) β 118 tools spanning the whole Hostinger control panel: VPS lifecycle, DNS zones, domain registration/transfer, billing, shared hosting, and email. Lets your AI helpers inspect and operate the very VPS this stack runs on. Bearer-gated through Caddy like the other MCPs.
Tool surface (118 tools, by area)
| Area | What it covers |
|---|---|
| VPS | List/inspect virtual machines, metrics, snapshots/backups, start/stop/restart, OS templates β and rebuild / delete. |
| DNS | Read and edit DNS zones + records (A / AAAA / CNAME / MX / TXT β¦) for your domains. |
| Domains | Availability checks, registration, transfers, nameserver + WHOIS management. |
| Billing | Catalog, orders, subscriptions, payment methods. |
| Hosting / Email | Shared-hosting accounts and mailbox management. |
.mcp.json entry for this server must point at the bare host https://mcp-hostinger.pocketcode.in/ β not .../mcp. Copying the postgres example and swapping the subdomain will 404; remember to drop the /mcp suffix.
code-server β browser VS Code
URL: https://code.pocketcode.in
Source (unit): ~/pocketcode-project/operations/host/systemd/code-server.service
Runs as: host systemd service β NOT a container. Same posture as ttyd.
Browser-accessible VS Code IDE β the same thing your Mac VS Code is, but running on the VPS. Lets you edit any file in ~/pocketcode-project/, control Docker, and run claude from the integrated terminal β all without a separate Mac install. Auth is via Caddy auth_gate (the master login cookie); code-server itself runs --auth none so there's no double-login.
What's pre-wired
- Workspace:
/root/pocketcode-projectopens by default. claudeon PATH: symlinked at/usr/local/bin/claude, also on$PATHvia~/.bashrc.- Env: systemd unit sources
secrets/secrets.env, soANTHROPIC_API_KEY,OPENROUTER_API_KEY, andMCP_BEARER_TOKENare present for the integrated terminal + Claude Code IDE extension. - Docker control: the integrated terminal can run
docker ps,docker exec ...,ai-doctor, etc. natively (no socket mount needed β it's the host).
Claude IDE extension
Install once via the IDE's Extensions panel (Ctrl+Shift+X) β search "Claude Code", install anthropic.claude-code. It reads your ANTHROPIC_API_KEY from the env or you can use the Pro/Max account OAuth flow. Open VSX rate-limits aggressively, so we don't auto-install at boot; manual install through the UI handles retries naturally.
Network posture
- Binds
0.0.0.0:8773on the host. - UFW: ALLOW
127.0.0.1+172.17.0.0/16+172.18.0.0/16; DENY anywhere else. Same model asttyd. - Caddy proxies
code.pocketcode.inβhost.docker.internal:8773. Service-wrapper iframe shell on top-level navigation; deep paths (WebSockets, assets) fall through to the upstream.
Restart it
systemctl restart code-server systemctl status code-server --no-pager | head -5