AdKit MCP Server
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@AdKit MCP Serverfind an ad for a camping tent"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
A simple MCP Server that serves advertisements to LLMs! Use this to Inject Advertisements from your sponsors in your LLM.
Introduction
AdKit is a lightweight semantic ad-matching engine built for LLM applications. It exposes a small, safe tool surface via MCP so agents can request relevant ads using natural-language context (chat turns, page content, search queries) — without brittle keyword rules.
Under the hood, AdKit embeds your context locally (FastEmbed) and retrieves candidates from Qdrant using vector similarity plus typed constraints (topics, locale, verticals, exclusions, policy flags). It’s designed with a hard security boundary: the Data Plane is read-only and allowlisted, while the Control Plane handles ingestion and admin operations separately.
Use it when you want “native” sponsor inserts or product recommendations that match meaning, not strings — and you want the architecture to stay sane when you ship to production.
Where can you use this?
AI Agents & Assistants: Seamlessly inject relevant product recommendations or sponsored messages into chat interfaces (e.g., customer support bots, shopping assistants).
RAG (Retrieval-Augmented Generation) Pipelines: Serve "sponsored context" alongside organic retrieval results, allowing for high-relevance native advertising in search or Q&A tools.
Content Discovery Platforms: Power "You might also like" features or affiliate link insertion based on the semantic meaning of the content being consumed, rather than fragile keyword matching.
OR take inspiration from the architecture
Prerequisites
Setup
Install uv
# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
# Or with pip
pip install uvStart Qdrant Locally
# Using Docker
docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant
# Or download binary from https://github.com/qdrant/qdrant/releasesInstall Dependencies
# Install all dependencies and create virtual environment
uv syncConfigure Environment (Optional)
# Copy example env file (defaults work for local Qdrant)
cp .env.example .envDemo ads setup (step-by-step)
Follow these steps in order to load demo ads and confirm everything works. Demo ads are defined in data/test_ads.json; re-running seed upserts them so the store matches the file.
Step 1. Start Qdrant (in a terminal):
docker run -d --name qdrant \
-p 6333:6333 -p 6334:6334 \
qdrant/qdrant
docker ps --filter name=qdrantStep 2. In the project directory, install dependencies:
uv syncStep 3. (Optional) Copy env:
cp .env.example .envStep 4. Create the collection:
uv run ad-index create If it exists (uv run ad-index delete)
Step 5. Load demo ads from the file:
uv run ad-index seedTo use a different file: uv run ad-index seed --file path/to/ads.json
Step 6. Verify it worked:
Run:
uv run ad-index infoConfirm Points count is 5 (or the number of ads in your JSON file).
Optionally, query ads from Python to confirm they are being served:
uv run python -c " from ad_injector.wiring import build_match_service from ad_injector.models.mcp_requests import MatchRequest r, _ = build_match_service().match(MatchRequest(context_text='python', top_k=2)) print(r.model_dump_json(indent=2)) "You should see matching ads (e.g. the Python/coding ad) in the output.
Architecture
The system is split into two MCP server planes:
Plane | Purpose | Who calls it | Entrypoint |
Data Plane | Ad matching, read-only retrieval | LLMs / agents |
|
Control Plane | Provisioning, ingestion, admin ops | Humans, CI/CD, backoffice |
|
Run two separate processes for production: one Control Plane (admin) and one Data Plane (runtime). Each has its own auth scope (optional MCP_ADMIN_KEY / MCP_DATA_KEY).
Data Plane tools (runtime, LLM-facing)
ads_match— semantic ad matching (context_text, placement, constraints, top_k); returns candidates and match_id for explainads_explain— audit trace for a prior match (match_id)ads_health— liveness/readiness (Qdrant + embedding)ads_capabilities— supported placements, constraint keys, embedding model, schema version
The Data Plane uses an explicit allowlist (DATA_PLANE_ALLOWED_TOOLS). No destructive or admin tools can be registered.
Control Plane tools (admin)
collection_ensure— create/align collection (dimension, embedding_model_id, schema_version)collection_info— collection metadata (points_count, dimension, embedding_model_id, schema_version)collection_migrate— optional schema migrations (from_version, to_version)ads_upsert_batch— batch ad ingestion (JSON array)ads_delete— delete an ad by idads_bulk_disable— set enabled=false for ads matching a filter (JSON filter)ads_get— fetch a single ad (debugging)
Repo structure
src/ad_injector/
models/ # Ad, Targeting, Policy; MCP request/response DTOs
services/ # MatchService, PolicyEngine, TargetingEngine, IndexService
adapters/ # QdrantVectorStore, FastEmbedProvider
mcp/ # server, tools, auth, observability
config/ # RuntimeSettings, env vars
ops/ # smoke_check, migrationsConfiguration
Runtime settings are managed via environment variables (or .env), validated at startup by Pydantic:
Variable | Default | Description |
|
| Qdrant server host |
|
| Qdrant server port |
|
| Collection name |
|
| Embedding model |
|
| Vector dimension |
|
| Max results per match query |
|
| Max ads per upsert batch |
|
| Per-request timeout |
|
| If true, Control Plane requires |
|
| If true, Data Plane requires |
Running with uv
Run Scripts
# Data Plane MCP server (LLM-facing, read-only): ads_match, ads_explain, ads_health, ads_capabilities
uv run ad-mcp-data
# or: uv run ad-data-plane
# Control Plane MCP server (admin): collection.*, ads.upsert_batch, ads.delete, ads.bulk_disable, ads.get
uv run ad-mcp-control
# CLI (Control Plane): create collection, seed ads, info, delete
uv run ad-index create # Create the collection
uv run ad-index seed # Add sample ads for testing
uv run ad-index info # Show collection info
uv run ad-index delete # Delete the collectionRun Python Files Directly
uv run python -m ad_injector.main_runtime # Data Plane MCP
uv run python -m ad_injector.main_control # Control Plane MCP
uv run python -m ad_injector.cli create # Control Plane CLI
uv run python -m ad_injector.cli seedNote: The seed command loads demo ads from data/test_ads.json (or --file <path>) and upserts them into the collection. Run create first to set up the collection, then seed to load the test data.
Validating the MCP servers
1. Run the test suite
uv run pytest tests/ -vThis runs the Data Plane guardrail tests which assert:
Data Plane exposes only the allowlisted tools (
ads_match,ads_explain,ads_health,ads_capabilities)No forbidden/destructive tools on the Data Plane
Control Plane has admin tools and does not expose Data Plane–only tools
2. Verify Data Plane exposes the allowlisted tools
uv run python -c "
from ad_injector.mcp.server import create_server
from ad_injector.mcp.tools import DATA_PLANE_ALLOWED_TOOLS
s = create_server('data')
tools = set(s._tool_manager._tools.keys())
print(f'Server: {s.name}')
print(f'Tools: {tools}')
assert tools == DATA_PLANE_ALLOWED_TOOLS, f'FAIL: expected {DATA_PLANE_ALLOWED_TOOLS}'
print('PASS: Data Plane allowlist registered')
"3. Verify Control Plane starts with admin tools
uv run python -c "
from ad_injector.mcp.server import create_server
s = create_server('admin')
tools = set(s._tool_manager._tools.keys())
print(f'Server: {s.name}')
print(f'Tools: {tools}')
assert 'ads_match' not in tools, 'FAIL: ads_match on admin plane'
assert 'collection_ensure' in tools
print('PASS: admin tools registered, no ads_match')
"4. Verify ads_match DTO validation
uv run python -c "
from ad_injector.models import MatchRequest, MatchConstraints, PlacementContext, MatchResponse, AdCandidate
# Valid request
req = MatchRequest(
context_text='I want to learn Python',
top_k=5,
placement=PlacementContext(placement='sidebar', surface='chat'),
constraints=MatchConstraints(topics=['python'], locale='en-US', sensitive_ok=False),
)
print(f'MatchRequest OK: context_text={req.context_text!r}, top_k={req.top_k}')
print(f' constraints.topics={req.constraints.topics}, locale={req.constraints.locale}')
# Valid response
resp = MatchResponse(
candidates=[AdCandidate(ad_id='ad-001', advertiser_id='adv-1', title='Learn Python',
body='Courses', cta_text='Go', landing_url='https://example.com', score=0.95, match_id='m-1')],
request_id='req-xyz', placement='sidebar',
)
print(f'MatchResponse OK: {len(resp.candidates)} candidate(s)')
# Invalid request (empty context) fails
try:
MatchRequest(context_text='', top_k=5)
print('FAIL: empty context_text should be rejected')
except Exception:
print('PASS: empty context_text rejected')
"5. Verify config loads and validates
# Defaults
uv run python -c "
from ad_injector.config import get_settings
s = get_settings()
print(f'host={s.qdrant_host} port={s.qdrant_port} model={s.embedding_model_id}')
"
# Invalid port fails fast
QDRANT_PORT=99999 uv run python -c "from ad_injector.config.runtime import RuntimeSettings; RuntimeSettings()" 2>&1 | head -36. Verify import isolation (Data Plane does not load admin code)
uv run python -c "
import sys
from ad_injector.main_runtime import main
mods = [m for m in sys.modules if m.startswith('ad_injector')]
assert 'ad_injector.cli' not in mods, 'FAIL: cli imported'
print('PASS: main_runtime has clean import graph (no admin modules)')
"Add Dependencies
uv add <package-name> # Add a dependency
uv add --dev <package-name> # Add a dev dependencyAd Schema
Each ad stored in Qdrant contains:
Field | Type | Description |
| string | Unique identifier for the ad |
| string | Identifier for the advertiser |
| string | Ad headline |
| string | Ad body text |
| string | Call-to-action text |
| string | Redirect URL |
| string[] | Topics to target |
| string[] | Locale codes (e.g., "en-US") |
| string[] | Industry verticals |
| string[] | Keywords to exclude |
| boolean | Sensitive content flag |
| boolean | Age restriction flag |
| boolean | Whether the ad is eligible for matching (default |
Embedding text: The vector embedding is generated from title + body + topics.
Usage Example
from ad_injector.models import Ad, AdTargeting, AdPolicy
from ad_injector.wiring import build_index_service, build_match_service
from ad_injector.models.mcp_requests import MatchRequest
# Create the collection (once) and seed ads via IndexService
index_svc = build_index_service()
index_svc.ensure_collection()
ad = Ad(
ad_id="ad-001",
advertiser_id="adv-123",
title="Learn Python Today",
body="Master Python programming with our interactive courses.",
cta_text="Start Learning",
landing_url="https://example.com/python",
targeting=AdTargeting(
topics=["programming", "python", "education"],
locale=["en-US"],
verticals=["education", "technology"],
),
policy=AdPolicy(sensitive=False, age_restricted=False),
)
index_svc.upsert_ads([ad])
# Match ads via MatchService (Data Plane logic)
match_svc = build_match_service()
response, audit_trace = match_svc.match(
MatchRequest(context_text="python tutorial", top_k=5)
)
for c in response.candidates:
print(f"{c.ad_id}: {c.title} (score={c.score}, match_id={c.match_id})")ads_match request / response schemas
The Data Plane ads_match tool uses typed DTOs — no raw dict filters are accepted.
Request parameters
Parameter | Type | Default | Description |
| string (1-10000 chars) | required | Conversational / page context to match against |
| int (1-100) |
| Number of candidates to return |
| string |
| Placement slot (e.g. |
| string |
| Surface type (e.g. |
| string[] | null |
| Restrict to these topics |
| string | null |
| Required locale (e.g. |
| string[] | null |
| Restrict to these verticals |
| string[] | null |
| Advertiser IDs to exclude |
| string[] | null |
| Ad IDs to exclude |
| bool |
| Allow age-restricted ads |
| bool |
| Allow sensitive-content ads |
Response shape
{
"candidates": [
{
"ad_id": "ad-001",
"advertiser_id": "adv-123",
"title": "Learn Python Today",
"body": "Master Python programming...",
"cta_text": "Start Learning",
"landing_url": "https://example.com/python",
"score": 0.95,
"match_id": "m-abc123"
}
],
"request_id": "req-xyz-456",
"placement": "sidebar"
}match_idcan be passed toads_explainfor audit traces (why eligible/ineligible, filters, scores)scoreis cosine similarity (0-1)
Talk to me
I’m always up for nerding out about MCP tooling, retrieval systems, and practical LLM monetization.
If you’re building something similar—or want to pressure-test your architecture—reach out:
🐦 Twitter: https://twitter.com/charoori_ai
📧 Email: mailto:chandrahas.aroori@gmail.com?subject=AdKit
This server cannot be installed
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/Exorust/Adkit-MCP'
If you have feedback or need assistance with the MCP directory API, please join our Discord server