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. It also has 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
Use it when you need grounded, first-party 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 5 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 also 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 is current, 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, where r is what is left and 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 is 600 requests per 60 seconds per client address, counted in the edge location that serves you. Every IPv6 address in one /64 counts as one client. 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 5 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 metered at 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. 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 endpoint validates the request exactly as it would a real one, then answers 200 with status: "validated". It sends no email and spends 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 depends on the Accept header. 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 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 5 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, so the document converts directly into 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",
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
"description": "Identifier to pass to getBlogPost."
}
},
"required": [
"slug"
]
}
}Conversation
The site also runs an AI assistant, Jarvis, grounded on this same content. Talk to it through the chat button on any page. Its WebSocket is private to that widget, has no published protocol and can change without notice. For a program, use the REST API or the MCP server above.
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, which is 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. server/discover (sent with 2026-07-28 _meta) lists the per-request revision; the older ones are negotiated by initialize. Every POST needs Content-Type: application/json; initialize-era messages also need Accept: application/json, text/event-stream, because their replies arrive as a one-event SSE stream. 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 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 such as an unknown slug, a bad email or a spent allowance come back as tool execution errors (isError), not as protocol errors. Arguments that fail a tool's inputSchema are reported field by field, for example Input validation error: Invalid arguments for tool send_message: email: must be a valid email address, message: must be between 20 and 4000 characters.
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. It is the one URL to probe if you know nothing else about this site.
In-browser tools (WebMCP)
Every page registers tools on document.modelContext when the browser supports the WebMCP draft: get_profile, list_blog_posts, get_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. |
The home, about, resume and blog pages and every post have a markdown version at index.md under the page's path, such as /about/index.md. The page's own URL returns the same markdown when Accept ranks text/markdown above text/html, by q-value and then by order. Other pages and other Accept headers get the HTML, whose Link header names the markdown version as rel="alternate" when there is one.
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.