mcp-tool-server
This server provides a FastMCP-compliant API with three utility tools and a REST health endpoint:
analyze_text: Computes word count, character count (including whitespace), sentence count (split on
.,!,?), average word length, and estimated reading time (based on 200 words per minute).fetch_url_metadata: Fetches an HTTP/HTTPS URL and returns the final URL (after redirects), HTTP status code,
Content-Type,Content-Length, and response time in milliseconds. HTTP error codes (4xx/5xx) are returned as data, not errors; only transport failures (DNS, timeout, connection refused) raise errors. Supports a configurable timeout (default: 10 seconds).convert_temperature: Converts temperature values between Celsius, Fahrenheit, and Kelvin, rejecting values below absolute zero.
/health REST endpoint: Always accessible without authentication; returns server status, name, version, and environment.
Additional capabilities include opt-in API key authentication for the MCP endpoint (supporting multiple bearer tokens), configurable transport (HTTP default or stdio), environment-based configuration (host, port, log level, API keys), Docker deployment, CI/CD via GitHub Actions, and integration with MCP clients or programmatic use via the FastMCP client library.
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., "@mcp-tool-serverCheck the response time and headers for https://example.com"
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.
mcp-tool-server
A production-shaped MCP tool server built with FastMCP 3, FastAPI, and Pydantic v2.
Three example tools, a REST health endpoint on the same port, opt-in API-key auth, 100% test coverage, CI that publishes a real image on every merge, and ready-to-use deployment configs for Fly.io, Cloud Run, Kubernetes, and Railway — built in nine incremental, independently-verified phases.
Contents
Related MCP server: simple-mcp-server
Why this exists
This is a reference/portfolio implementation, not a business application — the three tools exist to demonstrate patterns (sync vs. async execution, external I/O, structured validation, error translation, tool metadata) rather than to solve one specific problem. A few decisions worth knowing before you read the code:
Tools are decoupled from the MCP server instance. Each tool in
app/tools/builds a standaloneToolviaFunctionTool.from_function(...)instead of decorating an existingmcpobject.create_server()wires them in at construction time. This avoids a circular import betweenapp.serverandapp.tools, and makes every tool callable and unit-testable without any MCP machinery involved.Business logic doesn't know MCP exists.
app/services/has zero FastMCP imports and raises plainValueError. Translation toToolErrorhappens once, at theapp/tools/boundary.Dependencies were added when first used, not upfront. FastAPI wasn't added until the health endpoint needed it;
httpxanduvicornlikewise. Nothing sits unused in the lockfile.Auth is opt-in, not bolted on as an afterthought.
app/security/holds one job: verify a bearer token. It's aTokenVerifierFastMCP already knows how to consume, not a custom middleware reinventing that.A tool that fetches arbitrary URLs, called by an AI agent, is an SSRF vector by default.
fetch_url_metadataresolves the hostname and checks the actual IP before ever connecting — a caller (or a model prompt-injected into calling it) pointing it at169.254.169.254(cloud instance metadata) or an internal10.x/192.168.xaddress gets refused, not a credential leak. Scoped deliberately: this closes the direct case, not DNS rebinding, which needs transport-layer enforcement rather than a pre-request check like this one.Two ASGI apps, one port — see Architecture below.
Architecture
flowchart TD
A["Client (MCP client, curl, browser)"] --> B
subgraph B["Transport Layer"]
direction LR
B1["server.py\nstdio / FastMCP"]
B2["asgi.py\nFastAPI + /health + /mcp mount"]
end
B --> C
subgraph C["Adapter Layer"]
direction LR
C1["tools/\nMCP tool wrappers"]
C2["api/\nREST routes"]
end
C --> D["services/\npure business logic, no FastMCP import"]
D --> E["models/\nPydantic schemas"]
C -.uses.-> E
G["security/\napi-key verification"] -.guards.-> B
F["config/ + utils/\nsettings and logging"] -.injected into.-> B
F -.injected into.-> C
F -.injected into.-> G
classDef transport fill:#eef2ff,stroke:#4338ca,color:#1e1b4b
classDef adapter fill:#fff7ed,stroke:#c2410c,color:#431407
classDef core fill:#ecfdf5,stroke:#047857,color:#022c22
classDef cross fill:#f8fafc,stroke:#64748b,color:#0f172a,stroke-dasharray: 3 3
classDef security fill:#fef2f2,stroke:#b91c1c,color:#450a0a
class B1,B2 transport
class C1,C2 adapter
class D,E core
class F cross
class G securityDependency direction is one-way: transport → adapters → services → models.
Nothing in services/ or models/ imports anything above it — that's what
keeps business logic testable without spinning up any MCP or HTTP machinery.
.github/workflows/ # CI: lint + type-check + test, secret scan, Docker build/smoke-test, GHCR publish
fly.toml # Fly.io deployment config
railway.json # Railway deployment config
deploy/cloudrun/ # Cloud Run declarative service spec
docs/ # Deeper guides not in the main README (Claude API integration)
examples/ # Runnable integration examples, referenced from docs/
k8s/ # Kubernetes manifests (Deployment, Service, HPA, ConfigMap, Secret template)
assets/ # README charts (PNG, generated from real data -- see scripts/)
scripts/ # generate_charts.py: regenerates assets/*.png; not part of the app
app/
├── server.py # create_server(): builds the FastMCP instance + tool registry
├── asgi.py # create_asgi_app(mcp, settings): mounts FastMCP into FastAPI for the http transport
├── config/ # Environment-driven settings (Pydantic v2 / pydantic-settings)
├── utils/ # Logging and other cross-cutting helpers
├── security/ # Opt-in API-key bearer-token verification
├── api/ # Plain REST routes (currently just /health, never auth-gated)
├── tools/ # MCP tool adapters: schema, metadata, ValueError -> ToolError translation
├── services/ # Pure business logic. No FastMCP import, anywhere.
└── models/ # Pydantic schemas shared by tools/services/api
tests/ # 69 tests, 100% line coverage (unit + integration, no real network calls)Request lifecycle
The one genuine gotcha in this codebase: mcp.http_app() returns a
Starlette app whose session manager only starts if its lifespan is
explicitly handed to the parent FastAPI app. Skip that and /health
works fine while every tool call fails with a task-group error — confirmed
the hard way during Phase 3 (see app/asgi.py). The diagram below is what
it looks like once that's wired correctly:
sequenceDiagram
participant C as Client
participant F as FastAPI (asgi.py)
participant Auth as Auth (security/)
participant M as FastMCP session manager
participant T as Tool adapter
participant S as Service
C->>F: POST /mcp (tools/call)
F->>Auth: check Authorization header
alt auth disabled or token valid
Auth-->>F: ok
F->>M: routed via mounted app
M->>T: invoke tool function
T->>S: delegate to service
alt success
S-->>T: return value
T-->>M: Pydantic model
M-->>F: structured result
F-->>C: 200 + JSON-RPC result
else invalid input
S--)T: raise ValueError
T--)M: raise ToolError
M-->>F: JSON-RPC error
F-->>C: JSON-RPC error (is_error=true)
end
else missing or invalid token
Auth-->>F: reject
F-->>C: 401 Unauthorized
end/health is simpler — it never touches the MCP session manager, or the auth
check, at all; it's a plain FastAPI route that reads settings via dependency
injection (see Testing for a real bug that caught).
Authentication
Off by default, on by setting one environment variable:
MCP_API_KEYS=key-for-client-a,key-for-client-bEmpty (default): no auth. Anyone who can reach the port can call any tool. Fine for local exploration; not fine for anything reachable outside your own machine.
Set: bearer-token auth on
/mcp, enforced by FastMCP itself.app/security/api_key_auth.pyimplementsTokenVerifier— the resource-server pattern FastMCP already understands, not a hand-rolled middleware — checking each token against the configured set withhmac.compare_digest(constant-time, so a valid key can't be inferred faster via response-timing side channels)./healthis never gated, on purpose — infrastructure checking liveness (Docker'sHEALTHCHECK, a load balancer, a k8s probe) shouldn't need credentials just to know the process is up.Multiple keys, not one shared secret — supports rotation: issue a new key, roll it out, then remove the old one from
MCP_API_KEYSwithout any downtime.
This is deliberately a pre-shared-key scheme, appropriate for a small
number of known/trusted callers. fastmcp.server.auth.providers bundles
real OAuth/OIDC integrations (Auth0, WorkOS, GitHub, and others) for when
per-user identity or third-party client registration is actually needed —
reach for one of those rather than extending ApiKeyVerifier into
something it isn't.
from fastmcp import Client
async with Client("http://localhost:8000/mcp", auth="key-for-client-a") as client:
await client.call_tool("analyze_text", {"text": "Hello!"})Keeping keys out of logs and the repo, specifically:
Every configured/submitted token is logged, if at all, only through
mask_token()— a short prefix and a length ("a-re...(34 chars)"), enforced by tests that assert the real value never appears incaplogoutput for either a successful or failed check (tests/test_auth.py). Before this,verify_tokenlogged nothing at all about auth attempts — a real observability gap, not a deliberate safety choice, closed alongside the masking rather than left as a TODO.Confirmed directly (not assumed) that neither FastMCP's own 401 response body/headers nor its internal log line echo back a submitted token or a configured key — checked with a real request carrying a token designed to be obviously identifiable if it leaked.
.github/workflows/ci.ymlrunsgitleaksagainst full git history on every push and PR, andpublishwon't run if it finds anything — installed as the plain open-source binary directly rather than via the officialgitleaks-actionwrapper, which requires a paid license for organization (not personal) accounts as of v3. Verified locally against this exact repo (clean) and against a planted fake credential in an isolated scratch repo (correctly caught, exit code 1) before being wired into CI.No real key has ever existed in this repository —
.env.exampleships the variable empty,k8s/secret.example.yamlis obvious placeholder text and is deliberately excluded fromkustomization.yaml, and every value that looks like a key in tests or docs ("secret-key","key-for-client-a", ...) is a fixture, confirmed by runninggitleaksitself against the repo, not just by eyeballing it.
Tools
Tool | Style | Tags | Description |
| sync |
| Word/character/sentence counts and an estimated reading time. |
| async |
| Status code, headers, and response time for an http/https URL. Refuses hosts that resolve to a private/internal address (SSRF protection — see Why this exists). Only transport failures raise — a 404 is valid data. |
| sync |
| Convert between celsius, fahrenheit, and kelvin; rejects values below absolute zero. |
Each carries MCP tool annotations (readOnlyHint, idempotentHint /
openWorldHint) so clients can reason about side effects before calling them.
Quick start
uv sync
cp .env.example .env
uv run python -m app.serverThis starts the http transport on http://0.0.0.0:8000, serving both
/health and the MCP endpoint at /mcp. Set MCP_TRANSPORT=stdio in .env
instead if a client (e.g. Claude Desktop) will spawn this process directly.
Using with Claude Desktop
stdio is the transport an actual MCP client uses to spawn this process
directly (as opposed to connecting to an already-running http server) —
Settings → Developer → Edit Config opens claude_desktop_config.json
(~/Library/Application Support/Claude/ on macOS, %APPDATA%\Claude\ on
Windows). Add:
{
"mcpServers": {
"mcp-tool-server": {
"command": "uv",
"args": [
"run", "--directory", "/absolute/path/to/mcp-tool-server",
"python", "-m", "app.server"
],
"env": { "MCP_TRANSPORT": "stdio" }
}
}
}--directory matters, not just style — Claude Desktop spawns command
directly with no guaranteed working directory, and without it Settings'
relative .env lookup (and uv's own project discovery) would silently
fail depending on wherever Claude Desktop happens to run from. Use an
absolute path; fully quit and restart Claude Desktop (not just close the
window) after saving; a hammer/tools icon in the chat input confirms it
connected.
This transport surfaced a real bug no amount of http-transport testing
ever would have. configure_logging sent every log line — this app's
own, plus every third-party logger sharing the root logger, FastMCP's
included — to stdout. Under http that's invisible; stdout is just a
stream Docker or a terminal captures. Under stdio, stdout is the
JSON-RPC channel a client parses message-by-message, and every log line
between two real protocol messages is itself invalid JSON. A real client
connection confirmed the actual symptom directly: Failed to parse JSONRPC message from server on essentially every line, from fastmcp's own
client — which happened to keep working anyway, by skipping unparseable
lines and waiting for the next valid one. That's not something to depend
on; a stricter client has no obligation to be that forgiving. Fixed by
sending logs to stderr instead (app/utils/logging.py) — required for
stdio to work at all, and a no-op for every other target here, since
Docker/Kubernetes/Fly/Cloud Run/Railway all capture stderr right alongside
stdout. Verified before and after with a real spawned subprocess speaking
real stdio (not a mock): broken, then a single clean line of output with
zero log noise on stdout, confirmed by literally counting the lines.
Calling this from your own code against the Claude API (rather than
through the Claude Desktop app) is a related but different integration —
covered separately in
docs/claude-api-integration.md, with
two runnable examples in examples/.
Docker
cp .env.example .env
docker compose up --buildflowchart LR
subgraph Builder["Builder stage - python:3.12-slim"]
direction TB
U["uv binary, pinned 0.11.7"] --> L["uv sync --frozen --no-install-project"]
L --> Src["copy app/ source"]
Src --> Sync2["uv sync --frozen (installs project)"]
end
Sync2 -->|copy .venv + app/ only| Runtime
subgraph Runtime["Runtime stage - python:3.12-slim"]
direction TB
Venv[".venv"] --> User["non-root user"]
User --> Health["HEALTHCHECK -> /health"]
Health --> Cmd["CMD python -m app.server"]
endDependencies are synced in the builder stage from the lockfile before any
application code is copied in, so rebuilds only reinstall packages when
uv.lock actually changes. The runtime stage carries only the built virtual
environment and app/ — no uv, no lockfile, no tests, no dev dependencies.
# without compose
docker build -t mcp-tool-server .
docker run --rm -p 8000:8000 --env-file .env -e MCP_HOST=0.0.0.0 mcp-tool-serverNote: this Dockerfile follows the standard multi-stage
uvpattern and was reviewed carefully, but this sandbox had no Docker daemon available to actually rundocker buildagainst — unlike the rest of this project, it wasn't executed end-to-end here. Thedockerjob in.github/workflows/ci.ymlbuilds and smoke-tests the image on every push, which is where this actually gets verified — check that it's green before relying on the image.
CI/CD
Four jobs. test, secret-scan, and docker run on every push and PR to
main; publish only runs on push to main, and only if the other three
all pass:
Job | What it runs |
|
|
|
|
|
|
| Only on push to |
astral-sh/setup-uv and actions/checkout are pinned to a specific commit
SHA rather than a mutable tag (@v9.0.0 as a comment for readability, but
the SHA is what actually runs) — a floating tag can be repointed by whoever
controls the action's repo; a SHA can't. Standard supply-chain hardening
for anything that runs arbitrary code in CI.
Deployment
This is a stateless, single-process HTTP service with every setting externalized to environment variables — about as close to "deploy anywhere that runs a container" as an app gets. Four concrete targets are checked in rather than just described:
flowchart LR
Dev["git push main"] --> CI
subgraph CI["CI - .github/workflows/ci.yml"]
direction TB
T["test: ruff + mypy + pytest"] --> SS["secret-scan: gitleaks"]
SS --> D["docker: build + smoke test"]
D --> P["publish: build + push"]
end
P --> GHCR["ghcr.io/OWNER/mcp-tool-server"]
GHCR --> Fly["Fly.io\nfly.toml"]
GHCR --> CloudRun["Cloud Run\ndeploy/cloudrun/service.yaml"]
GHCR --> K8s["Kubernetes\nk8s/"]
Dev -.Dockerfile, no GHCR needed.-> Railway["Railway\nrailway.json"]
classDef ci fill:#eef2ff,stroke:#4338ca,color:#1e1b4b
classDef registry fill:#fff7ed,stroke:#c2410c,color:#431407
classDef target fill:#ecfdf5,stroke:#047857,color:#022c22
class T,SS,D,P ci
class GHCR registry
class Fly,CloudRun,K8s,Railway targetThe publish job in CI builds and pushes to GHCR on every merge to main
(after test and docker both pass, not before) — that's what makes the
manifests below reference a real, pullable image instead of a hypothetical
one. Replace OWNER in each file with the actual GitHub owner/repo.
Target | File | Notes |
Fly.io | Fastest path to a live URL. | |
Cloud Run | Declarative Knative spec; | |
Kubernetes |
| |
Railway | Detects the Dockerfile directly — connect the repo and it deploys, no GHCR step needed. |
Railway specifically needed a real code fix, not just a config file.
Fly/Cloud Run/Kubernetes all let you pick a fixed port and configure the
platform to route to it. Railway instead assigns a port dynamically and
injects it as a bare PORT env var with no way to rename it — and this
app only read MCP_PORT. Without a fix, Railway would've considered the
deploy healthy at the container level while every request 404'd at the
edge, since traffic would arrive on the port Railway picked while the
app listened on 8000 regardless. Fixed in app/config/settings.py with
validation_alias=AliasChoices("MCP_PORT", "PORT") — MCP_PORT still
wins if both are set, so this is purely additive for every other target.
One second-order bug surfaced while fixing the first: the Dockerfile baked
MCP_PORT=8000 in as an image-level default, which — since MCP_PORT
takes precedence — would have silently shadowed Railway's real port on
every deploy regardless of the fix above. Removed; app/config/settings.py
already defaults to 8000 on its own with no env var set at all, so the
Dockerfile default was pure redundancy that happened to also be actively
wrong for this one target. Verified with a real boot under simulated
Railway conditions (PORT set, MCP_PORT deliberately absent) — the app
came up on the assigned port and, just as importantly, confirmed not
listening on 8000 at all.
Secrets, consistently: MCP_API_KEYS is never written into any of
these files. Fly uses fly secrets set; Cloud Run uses Secret Manager via
--set-secrets (a commented secretKeyRef block shows where); Kubernetes
uses a separately-created Secret that the Deployment references with
optional: true; Railway uses its dashboard/CLI Variables (railway variables set) — so the app runs identically with or without it,
matching Authentication's "empty means disabled"
default.
On graceful shutdown: uvicorn drains in-flight requests on SIGTERM by
default; this was exercised (not just assumed) throughout development via
timeout N uv run python -m app.server, which sends SIGTERM and waits —
every such run exited cleanly on its own well within the timeout, with no
forced SIGKILL needed. The Kubernetes manifest's preStop hook and
terminationGracePeriodSeconds: 30 build in margin around that same path
for rolling updates specifically.
Same caveat as the Dockerfile itself: these four configs were written carefully and cross-checked against each platform's current documentation (both Fly's and Railway's schemas have changed before), and validated for syntax — but none were applied against a real Fly app, GCP project, or cluster from this sandbox. Treat first deploys accordingly.
Configuration
All variables are optional and prefixed MCP_; see .env.example for the
full, commented list. The ones worth knowing about:
Variable | Default | Notes |
|
|
|
|
| Only used for the |
|
|
|
|
| Standard library log level name |
| (empty) | Comma-separated bearer tokens; empty disables auth entirely — see Authentication |
|
| FastMCP pings PyPI on startup by default; set |
Example usage
Health check:
curl http://localhost:8000/health
# {"status":"ok","name":"mcp-tool-server","version":"0.1.0","environment":"development"}Calling a tool — the MCP endpoint is a stateful, session-based protocol
(not plain REST; see Request lifecycle), so the
practical way to call it is fastmcp's own client rather than raw curl:
import asyncio
from fastmcp import Client
async def main():
async with Client("http://localhost:8000/mcp") as client:
result = await client.call_tool(
"convert_temperature",
{"value": 100, "from_unit": "celsius", "to_unit": "fahrenheit"},
)
print(result.data) # output_value=212.0 ...
asyncio.run(main())Or in-memory, against the server object directly (no network at all — this is exactly what the test suite does):
from fastmcp import Client
from app.server import mcp
async with Client(mcp) as client:
tools = await client.list_tools()Testing
uv run pytest # 69 tests, coverage report on by default (see pyproject.toml)
uv run ruff check .
uv run mypy appThe same three commands run in CI on every push — see CI/CD.
Real milestones, not a smoothed curve — Phases 5-7 (Docker, docs, CI/CD)
genuinely added no new Python tests, and the chart shows that flat instead
of hiding it. The two jumps are Phase 4 (closing coverage gaps found by
--cov-report=term-missing) and Phase 8 (auth plus the masked-logging
hardening that came out of testing it). The final point is a refine pass
over the finished project — see below.
Coverage is 100% across all 275 statements in app/, and pyproject.toml
sets fail_under = 100 so that claim is enforced, not just true today by
coincidence: uv run pytest exits non-zero the moment coverage drops below
100%, verified with a positive control (a deliberately uncovered function,
confirmed to fail the build, then removed) rather than taken on faith. That
number is a byproduct of testing real behavior (every tool's error path,
the settings validation, the main() transport dispatch, the FastAPI
lifespan wiring), not a target chased for its own sake — the last few
percentage points came directly from pytest --cov-report=term-missing
pointing at genuine gaps, including two real bugs it caught:
create_asgi_app(settings)accepted asettingsargument that the health route was silently ignoring in favor of the global cached singleton (FastAPI'sDepends(get_settings)doesn't know about asettingsvalue constructed elsewhere unless you override the dependency) — fixed with a dependency override inapp/asgi.py.Testing auth by connecting
fastmcp.Clientdirectly to the in-memoryFastMCPinstance silently passed with no token required, regardless of configuration — because that transport bypasses HTTP (and therefore headers) entirely, not because auth was broken. It also revealedcreate_asgi_appwas closing over a module-level app built from default settings at import time, so passing different auth config into it did nothing. Both are fixed:create_asgi_app(mcp, settings)now takes the server instance explicitly, and auth tests go through real HTTP viaTestClientwith an actualAuthorizationheader (tests/test_auth.py).A later refine pass over the (by then "finished") project found two more, of a different kind — not missing tests, but dead or misleading surface area that had accumulated:
is_productionwas defined, tested, and never actually used anywhere, andMCP_TRANSPORTaccepted"sse"as a value that silently did nothing (never forwarded tomcp.http_app()). Fixed by wiringis_productionto actually disable FastAPI's/docs,/redoc, and/openapi.jsonin production, and by removing"sse"rather than leaving an option on that doesn't work — offering a config value that's silently a no-op is worse than not offering it. Also caught, in the same pass: a Dockerfile comment describing theMCP_PORT/PORTfallback precedence backwards from what the code directly below it actually did.A further pass, reading
fetch_url_metadataspecifically with a security lens rather than a correctness one, found the SSRF gap described above. Fixing it exposed a second issue in the fix itself: the obvious implementation does real DNS resolution before ever touching the (correctly mocked) HTTP client, which would have silently made every existingMockTransport-based test in this file dependent on real DNS working from wherever the suite happened to run — exactly what mocking the transport was supposed to prevent, just one layer down. Fixed by injecting the resolver the same way the HTTP client already was, then verified the fix against the actual attack it closes: a real MCP tool call to169.254.169.254(cloud metadata) through the full stack, blocked; a real call to a genuine public site, unaffected.
Bar colors in the first chart match the architecture diagram above —
green for the core layer (services/models), orange for adapters
(tools/api), indigo for transport, red for security — so the two visuals
read as one system rather than two unrelated ones. services carries the
most weight (79 statements) for the same reason it's the layer this
project cares most about testing in isolation: it's where the actual
business logic lives, deliberately kept free of any FastMCP import (see
Why this exists). test_web_service.py is the largest
test file — 17 cases from 11 test functions, since the SSRF check is
verified with a @pytest.mark.parametrize matrix across seven different
blocked-address ranges rather than one test per range copy-pasted; by raw
function count test_auth.py (15) is still larger, which tracks with why
auth got its own dedicated section — it's the other
piece of this codebase with real security consequences if it's wrong.
Both charts are generated from real, freshly-measured data (a live
pytest --cov run, grep -c "def test_" across tests/*.py, and the
exact milestone numbers observed during this project's own build — see
the growth chart above) by scripts/generate_charts.py,
not hand-drawn. Re-run it after the test suite changes enough to make the
numbers stale.
Network-dependent tests (test_web_service.py) use httpx.MockTransport —
no real HTTP calls, no flakiness, no dependency on network access in
whatever environment runs the suite.
Roadmap
Phase 1 — Architecture & project initialization
Phase 2 — Example tools + tool metadata
Phase 3 — FastAPI mounting + health endpoint
Phase 4 — Full unit test suite
Phase 5 — Docker + docker-compose
Phase 6 — Full documentation pass
Phase 7 — CI/CD (GitHub Actions: lint/type/test + Docker build & smoke test)
Phase 8 — Opt-in API-key authentication for the MCP endpoint (+ masked audit logging,
gitleaksin CI)Phase 9 — Deployability: GHCR image publish + Fly.io / Cloud Run / Kubernetes / Railway configs
Refine pass — no new features; closed gaps a fresh critical read found in the "finished" project: dead
is_productionproperty (now wired to gate/docsin production), aMCP_TRANSPORTvalue that silently did nothing, a backwards precedence comment in the Dockerfile,fail_under = 100so the coverage claim is enforced rather than just currently true, an SSRF gap infetch_url_metadata(arbitrary-URL tools called by AI agents are a real attack surface) closed with DNS-resolution-based address checking, and application logging corrupting thestdioJSON-RPC channel — invisible under everyhttp-based test in this whole build, found only by actually connecting a real client over realstdio(see Using with Claude Desktop)
License
MIT — see LICENSE.
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- FlicenseAqualityDmaintenanceA lightweight MCP server providing utility tools for math, text processing, data conversion, and URL fetching. It supports both STDIO and SSE communication modes for seamless integration with Claude Desktop and remote AI agents.51
- AlicenseNot gradedqualityDmaintenanceA simple MCP server offering three utility tools: UUID generation, temperature conversion, and text statistics.11MIT
- FlicenseNot gradedqualityCmaintenanceA lightweight MCP server providing tools for adding integers, getting current time, and fetching weather forecasts via wttr.in.
- FlicenseNot gradedqualityDmaintenanceA simple MCP server that provides basic utility tools for text manipulation, file operations, and calculations, intended to be connected to Claude AI desktop app.
Related MCP Connectors
Remote MCP server: 10 developer utilities (base64, JWT, DNS, UUID, URL, JSON, UA, IP lookup).
MCP server for URL shortening and management
MCP server for generating rough-draft project plans from natural-language prompts.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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/Kartik281204/MCP-Tool-Server'
If you have feedback or need assistance with the MCP directory API, please join our Discord server