murugappan.dev API — developer portal
A public, unauthenticated JSON API over everything this site publishes about Murugappan M — profile, work experience, skills, education, open-source work and blog posts — plus one write endpoint for passing along an opportunity. Built for AI agents and for anyone who would rather parse JSON than scrape HTML. This page is the whole developer reference: endpoints, authentication, versioning and deprecation, rate-limit headers, the OpenAPI 3.1.0 specification and the murugappan.dev MCP server.
- OpenAPI 3.1.0 spec
- MCP server
- MCP manifest
- API catalog
- GET /api/v1/profile
- Version policy
- llms.txt
- AGENTS.md
When to use this API
Reach for it when you need grounded facts about Murugappan M as a candidate or a collaborator:
- “Who is this person?” — one call to
GET /api/profilereturns the pitch, current role, location and every public link. - “Has he actually shipped X?” —
GET /api/experiencegives dated roles with concrete, per-role achievements, andGET /api/open-sourcelinks the individual merged pull requests so a claim can be checked at the source. - “Does he know technology Y?” —
GET /api/skillsanswers from a typed list instead of an inference over prose. - “What has he written about Z?” —
GET /api/posts?q=…searches titles and summaries;GET /api/posts/{slug}returns a post's full markdown for quoting or summarising. - “Reach out about a role.” —
POST /api/contactdelivers a message to his inbox. Use it for one concrete, specific opportunity on a human's behalf.
When not to use it. It is not a search engine, a resume parser or a job-matching service, and it holds data about exactly one person. For a conversation rather than a payload, use the chat socket below.
Quickstart
No signup, no key, no client library. Base URL https://murugappan.dev/api/v1 — see Versioning for why the version is in the path and why /api/… works too. Every response is JSON with permissive CORS, so a browser-side agent can call it directly.
curl -s https://murugappan.dev/api/v1/profileSearch the blog and read one post:
curl -s https://murugappan.dev/api/v1/posts?q=rate+limiting
curl -s https://murugappan.dev/api/v1/posts/cloud-agnostic-rate-limitingValidate a contact payload without sending anything (see Sandbox):
curl -s https://murugappan.dev/api/v1/contact \
-H 'Content-Type: application/json' \
-d '{
"name": "Ada Lovelace",
"email": "ada@example.com",
"company": "Analytical Engines",
"message": "We are hiring a senior backend engineer for a healthcare data platform.",
"dryRun": true
}'Endpoints
| Operation | Endpoint | What it returns |
|---|---|---|
getProfile | GET /api/v1/profile | Get the full profile |
listExperience | GET /api/v1/experience | List work experience |
listSkills | GET /api/v1/skills | List skills and proficiencies |
listEducation | GET /api/v1/education | List education |
listOpenSourceContributions | GET /api/v1/open-source | List open-source contributions |
listBlogPosts | GET /api/v1/posts | List blog posts |
getBlogPost | GET /api/v1/posts/{slug} | Get one blog post with its full markdown |
sendContactMessage | POST /api/v1/contact | Send Murugappan M a message |
getApiVersions | GET /api/v1/versions | Get the version and deprecation policy |
getOpenApiSpec | GET /api/v1/openapi.json | Get this OpenAPI document |
Full parameter and response schemas — including the ones the table summarises — live in the OpenAPI document.
Authentication & API keys
There are none, by design. Every endpoint is public and unauthenticated: no API key, no token, no signup, nothing to rotate. If a client asks you for credentials for this API, it is misconfigured. Read endpoints are cached for five minutes at the edge.
Versioning & deprecation policy
The version is a path segment: every endpoint lives under /api/v1. There is no version header and no version query parameter — the URL is the version. The unversioned /api/… prefix is a permanent alias for v1 and will never be repointed at a later major version, so both forms are safe to hard-code.
- The version is a path segment: every endpoint lives under /api/v1. There is no version header and no version query parameter — the URL is the version.
- The unversioned /api/... prefix is a permanent alias for v1 and will never be repointed at a later major version. Code against either; both keep answering v1 for as long as v1 exists.
- Additive changes ship inside a version without notice: new endpoints, new optional request fields, new response fields. Ignore fields you do not know rather than rejecting them.
- Breaking changes never ship inside a version. Removing or renaming a field or endpoint, changing a field's type, narrowing an enum, or changing what a status code means all require a new path version.
- A deprecated version answers every request with a Deprecation header (RFC 9745), a Sunset header (RFC 8594), and Link relations of deprecation and successor-version. At least 180 days pass between the first Deprecation header and the Sunset date.
- Every response carries API-Version (the release being served) and API-Supported-Versions. /api/v1/versions is the machine-readable form of this policy and is also reachable at /api/versions.
- After sunset a version answers 410 Gone with the standard error envelope, pointing at its successor. Paths are never silently reused.
Every response names the release it served and the versions still answering:
API-Version: 1.0.0
API-Supported-Versions: v1
Link: </api/v1/versions>; rel="version-history"Once a version is deprecated, every one of its responses additionally carries Deprecation (RFC 9745), Sunset (RFC 8594) and Link relations deprecation and successor-version:
Deprecation: @1798761600
Sunset: Thu, 01 Jul 2027 00:00:00 GMT
Link: </developers/#versioning>; rel="deprecation", </api/v2>; rel="successor-version"At least 180 days pass between the first Deprecation header and the sunset date; after sunset the version answers 410 with the usual error envelope pointing at its successor. Nothing is deprecated today — v1 iscurrent, released 2026-08-25. The whole of this section is served as data at /api/v1/versions (and /api/versions), so a client can check it without parsing this page.
Rate limits & rate-limit headers
Every response carries the IETF RateLimit header fields, so a client can self-throttle without reading this page: RateLimit-Policy lists the quota policies that apply, and RateLimit is the live snapshot of the one closest to exhaustion — r is what is left, t is seconds until it resets. The de-facto X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset trio mirrors the same numbers for tooling that only looks for those, and a 429 adds Retry-After.
RateLimit-Policy: "reads";q=600;w=60
RateLimit: "reads";r=599;t=60Reads are effectively unmetered: the fair-use ceiling is600 requests per 60 seconds per client address, counted in the edge location that serves you — a client spread across several locations gets that allowance in each, so the advertised number is a floor, not a cap. Read responses are cacheable for five minutes, so a cached response may carry a stale RateLimit snapshot; treat it as advisory and trust the freshest one you have.
POST /api/v1/contact sends real email, so it is genuinely metered:3 requests per client IP per UTC day and 20 site-wide. Past either it answers 429 with code rate_limited, a Retry-After counting down to 00:00 UTC, and the tier that ran out:
RateLimit-Policy: "contact-client";q=3;w=86400, "contact-site";q=20;w=86400
RateLimit: "contact-client";r=0;t=39122
Retry-After: 39122A rejected request never spends a slot, and neither does a dry run — but a dry run still reports the allowance, so it is the cheapest way to size a real send. All of these headers are listed in Access-Control-Expose-Headers, so a browser-side agent can read them too.
Sandbox
The read endpoints are the sandbox: they are public, unauthenticated and side-effect free, so production is safe to explore as much as you like — there is no separate environment to provision.
The one endpoint with a side effect has a dry-run mode instead. Send "dryRun": true to POST /api/contact and the request is validated exactly as a real one would be, then answered 200 with status: "validated" — no email, no rate-limit slot. Repeat until it validates, then resend without the flag.
Errors
Every failure under /api — including a 404 on a path that does not exist — returns the same JSON envelope. No HTML error page is ever served under /api. Branch on error.code, not on the message.
{
"error": {
"code": "not_found",
"message": "There is no API endpoint at /api/v1/nope.",
"hint": "Fetch https://murugappan.dev/openapi.json for the full list of endpoints.",
"documentation_url": "https://murugappan.dev/developers/"
}
}Codes: not_found, method_not_allowed, invalid_request, unsupported_media_type, payload_too_large, rate_limited, service_unavailable, internal_error. Field-level validation failures add a details array naming each offending field and what it must satisfy, so a function-calling model can repair its own arguments and retry.
Off the API, a path that does not exist answers a real 404 — never a 200 with the app shell, which would make every URL look valid. The body is content-negotiated: a browser gets the styled 404 page, and anything that did not ask for HTML gets a short markdown body listing the sitemap, this portal, the OpenAPI document, the API catalog and the MCP manifest, so a guessed or stale URL can be recovered from in one request.
Webhooks & callbacks
There are none, and that is deliberate: this API has no events to push. Content changes when the site redeploys, so poll instead — read endpoints are cacheable for five minutes, the blog RSS feed announces new posts, and GET /api/v1/posts is one cheap call. POST /api/v1/contact delivers by email and returns no callback; Murugappan replies to the address you supply.
Function calling
Every operation in the spec carries a unique operationId, a description, typed parameters and a response schema, which is what makes the document directly convertible to tool definitions. Point your OpenAPI-to-tools converter at https://murugappan.dev/openapi.json; the operationId becomes the tool name. For example, getBlogPost yields:
{
"name": "getBlogPost",
"description": "Returns a single post's metadata together with its complete markdown source (frontmatter included), so an agent can quote or summarise it without scraping HTML. Slugs come from `listBlogPosts`.",
"input_schema": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"description": "Identifier to pass to getBlogPost.",
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
}
},
"required": [
"slug"
]
}
}Conversational endpoint (WebSocket)
The site also runs an AI assistant, Jarvis, grounded on this same content. It speaks WebSocket at wss://murugappan.dev/parties/chat-room/{roomId}, which OpenAPI cannot describe — so it is documented here instead. Pick any room id; the room persists its history.
const ws = new WebSocket("wss://murugappan.dev/parties/chat-room/my-room-id");
ws.onmessage = e => console.log(JSON.parse(e.data)); // history | delta | done
ws.onopen = () =>
ws.send(JSON.stringify({type: "chat", text: "What has Murugappan shipped?"}));Frames in: {"type":"chat","text":"…"}. Frames out: history, visitor, delta, done, limit, error. Prefer the REST API when you want structured data, and the socket when you want a conversation.
MCP server
The same content is served as a Model Context Protocol server. Add it to any MCP client as:
https://murugappan.dev/mcpStreamable HTTP, POST only, no authentication and no session. It implements protocol revision 2026-07-28 — stateless, with per-request metadata — and keeps backward compatibility with the initialize-based revisions (2025-11-25, 2025-06-18, 2025-03-26) that most clients still speak. Call server/discover to see what a given deployment supports. There is no GET stream and nothing to DELETE; both answer 405.
| Tool | What it does |
|---|---|
get_profile | Profile of Murugappan M |
list_experience | Work experience |
list_skills | Skills and proficiencies |
list_education | Education |
list_open_source | Open-source contributions |
search_blog_posts | Search the blog |
get_blog_post | Read a blog post |
send_message | Send Murugappan M a message |
It also serves resources — documents a client can attach as context directly, rather than calling a tool for them: /llms.txt, /AGENTS.md, the OpenAPI specification, the full blog text, and every post's markdown (URI template https://murugappan.dev/blog/{slug}/index.md). They use https:// URIs because they really are fetchable straight from the web, which is what that scheme is reserved for.
Every tool declares a typed, self-contained outputSchema (the REST response schemas, inlined), so a client can validate structuredContent without fetching anything else. Recoverable problems — an unknown slug, a bad email, a spent allowance — come back as tool execution errors with text explaining the fix, not as protocol errors.
curl -s https://murugappan.dev/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/call' \
-H 'Mcp-Name: get_profile' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_profile",
"arguments": {},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}'Manifest
For a client that installs servers from a manifest rather than a pasted URL, the server's server.json is published at /.well-known/mcp.json (and at /mcp.json). It follows the published server.json schema, names the server dev.murugappan/murugappan-dev in the reverse-DNS form the registry requires, and declares one streamable-http remote:
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
"name": "dev.murugappan/murugappan-dev",
"version": "1.0.0",
"remotes": [{"type": "streamable-http", "url": "https://murugappan.dev/mcp"}]
}The server is also listed in the site's API catalog, the RFC 9727 link set that names every API here and where each one is described — the one URL to probe if you know nothing else about this site.
In-browser tools (WebMCP)
Every page registers tools on navigator.modelContext when the browser supports the WebMCP draft: get_profile, list_blog_posts, read_blog_post and navigate_to. A browser agent already on the page can call those without any HTTP of its own.
Machine-readable files
| URL | What it is |
|---|---|
/.well-known/api-catalog | RFC 9727 link set naming every API this site publishes and where each one is described. The one URL to probe if you know nothing else. |
/.well-known/mcp.json | MCP server manifest (server.json), for a client that installs MCP servers from a manifest. |
/mcp.json | The same manifest at the site root. |
/mcp | Model Context Protocol server (Streamable HTTP, POST only, no auth). |
/openapi.json | OpenAPI 3.1.0 description of this API — the canonical machine-readable contract. |
/api/v1/openapi.json | The same document under the versioned API prefix, for clients that look there. |
/api/v1/versions | The version catalogue and deprecation policy, as data. Also at /api/versions. |
/llms.txt | One-page summary of the whole site, including every blog post, with a 'when to use' section for agents. |
/AGENTS.md | Agent instructions: when to reach for this site, which call to make, and what not to use it for. |
/blog/llms-full.txt | Full text of every blog post in one file. |
/sitemap.xml | Every indexable URL on the site. |
/resume.pdf | ATS-parseable resume, generated from the same data this API serves. |
Any page URL also serves markdown when requested with Accept: text/markdown.
Stability & contact
Spec version 1.0.0, serving API version v1 — the full versioning and deprecation policy says what may change inside a version and how a deprecation is announced. Data is licensed CC BY 4.0 — cite murugappan.dev. Questions, or something broken? Email Murugappan or use POST /api/contact.