Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm

- run: pnpm install --frozen-lockfile

- run: pnpm format:check
- run: pnpm typecheck
- run: pnpm test
- run: pnpm build
32 changes: 32 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Publish

on:
release:
types: [published]

jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
registry-url: https://registry.npmjs.org

- run: pnpm install --frozen-lockfile

# Pre-publish guard: same gates as CI.
- run: pnpm typecheck
- run: pnpm test
- run: pnpm build

- run: pnpm publish --access public --no-git-checks
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
20
19 changes: 15 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ Guidance for Claude Code (claude.ai/code) when working in this repository.

**This is a public repository.** Keep it self-contained — do not reference internal or private Agentailor projects in code, comments, README, or commits. Scope is public data only; no credentials are required to run.

### Distribution

Meant to be used **without cloning**. Two first-class paths:

- **npm package `@agentailor/mcp`** — `npx @agentailor/mcp` runs the stdio server (Claude Desktop). The `bin` maps to `dist/index.stdio.js`, which carries a `#!/usr/bin/env node` shebang.
- **Hosted HTTP** — the HTTP transport runs on a native Node host: build `pnpm install --frozen-lockfile && pnpm build`, start `pnpm start:http`. `GITHUB_TOKEN` is set as a secret; `PORT` is injected by the host; health check is `/health`. Node 20 pinned via `engines` + `.node-version`.

Clone-and-run is a secondary/contributor path. The `Dockerfile` is for local stdio and self-hosting anywhere.

**Releasing:** bump `version` in `package.json` (and `src/server.ts`), commit, then publish a **GitHub Release** — the `.github/workflows/publish.yml` workflow runs the gates and `pnpm publish`es to npm (auth via the `NPM_TOKEN` repo secret). `.github/workflows/ci.yml` runs format/typecheck/test/build on push + PR.

## Commands

```bash
Expand Down Expand Up @@ -35,8 +46,8 @@ One codebase, two transports, five tools.

Both import the shared server from `src/server.ts` and differ only in the `server.start(...)` call:

- **stdio** — for local clients (Claude Desktop). Run via Docker.
- **httpStream** — self-hosted; MCP endpoint at `POST /mcp`, plus `GET /health` (enabled in `server.ts`).
- **stdio** — for local clients (Claude Desktop). Run via `npx @agentailor/mcp` (or Docker). The entrypoint has a `#!/usr/bin/env node` shebang so it works as a `bin`.
- **httpStream** — hosted/self-hosted; MCP endpoint at `POST /mcp`, plus `GET /health` (enabled in `server.ts`). Reads `PORT` from the environment.

### Server (`src/server.ts`)

Expand All @@ -52,7 +63,7 @@ Registered by workflow, not by endpoint. Names are prefixed `agentailor_` to avo
### Shared core

- `src/config.ts` — fetch targets (`BLOG_BASE_URL`, `GITHUB_ORG`), safety regexes, and defaults. Env overrides: `BLOG_BASE_URL`, `GITHUB_ORG`, `PORT`, `GITHUB_TOKEN`.
- `src/http.ts` — `fetchTextCached` (process-lifetime cache), `githubHeaders` (adds `Authorization` only when `GITHUB_TOKEN` is set), and error types: `RateLimitError` (GitHub 403-with-`x-ratelimit-remaining: 0` or 429), `NotFoundError` (404).
- `src/http.ts` — `fetchText` (plain text fetch, no cache — content is always fresh; rely on `GITHUB_TOKEN` for the API limit), `githubHeaders` (adds `Authorization` only when `GITHUB_TOKEN` is set), and error types: `RateLimitError` (GitHub 403-with-`x-ratelimit-remaining: 0` or 429), `NotFoundError` (404).
- `src/content.ts` — `Post` model, `getPosts()` (fetches/parses `llms.json`), and the pure `searchPosts(posts, params)` filter (query + tags + guidesOnly).
- `src/format.ts` — `formatSearchResults` with `concise`/`detailed` shapes and an overflow hint.
- `src/validate.ts` — pure input validation (`isBlogArticleUrl`, `isSafeRepo`, `normalizeDocPath`). Tools throw `UserError` on invalid input so the message reaches the agent.
Expand All @@ -77,7 +88,7 @@ Vitest, offline and deterministic (network is mocked via `vi.spyOn(globalThis, "

## Docker

One `Dockerfile`, `TRANSPORT` build ARG selects the entrypoint:
The `Dockerfile` is for local stdio and self-hosting anywhere. `TRANSPORT` build ARG selects the entrypoint:

```bash
docker build -t agentailor-mcp . # stdio (default)
Expand Down
5 changes: 3 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Multi-stage build. TRANSPORT selects the entrypoint:
# Multi-stage build for running the server locally or on any container host.
# TRANSPORT selects the entrypoint:
# stdio (default) — local clients (Claude Desktop) via `docker run -i`
# http — self-hosted server on PORT (default 3000)
# http — self-host anywhere on PORT (default 3000)
ARG TRANSPORT=stdio

FROM node:20-alpine AS builder
Expand Down
126 changes: 81 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

An MCP server that connects your AI assistant to [Agentailor](https://agentailor.com) — search the blog, read articles in full, and pull docs from the open-source repos, all over the Model Context Protocol.

It ships in two flavors from one codebase:
Two ways to use it, no clone required:

- **stdio** (via Docker) — for local clients like Claude Desktop.
- **HTTP** (streamable) — for a self-hosted, remotely reachable endpoint.
- **Hosted HTTP** — point your client at Agentailor's endpoint. Zero install.
- **npx (local stdio)** — run `npx @agentailor/mcp` as a command in Claude Desktop and other local clients.

Scaffolded with [`@agentailor/create-mcp-server`](https://www.npmjs.com/package/@agentailor/create-mcp-server) on [FastMCP](https://github.com/punkpeye/fastmcp) — the tool building its own ecosystem.

Expand All @@ -21,96 +21,132 @@ Scaffolded with [`@agentailor/create-mcp-server`](https://www.npmjs.com/package/

Scope is Agentailor's public content: the blog and the open-source org repos.

## Designed to the "Writing Effective Tools for AI Agents" playbook

These tools follow the five principles from Agentailor's [Writing Effective Tools for AI Agents](https://blog.agentailor.com/posts/writing-tools-for-ai-agents) — here's exactly how:
## Use it

| Principle | How this server applies it |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **1. Strategic selection** | One `agentailor_search_articles` covers discovery by topic, by tag, and guides-only — instead of four fragmented search tools. Tools map to how you actually work, not to endpoints. |
| **2. Clear namespacing** | Every tool is prefixed `agentailor_`, so it never collides with other MCP servers on your client. |
| **3. Meaningful context** | Read tools take `response_format: "concise" \| "detailed"`. Results carry titles, tags, and summaries you can reason on — not bare IDs. |
| **4. Token efficiency + guiding errors** | Search defaults to 20 results with an overflow hint that tells the agent how to narrow. A GitHub rate-limit returns "set a `GITHUB_TOKEN`" guidance, not a raw 403. |
| **5. Descriptions as prompts** | Each tool description includes a **When to use** section with example calls, and every parameter documents its shape with an example. |

## Quick start (local)

```bash
pnpm install
pnpm build
### Hosted (no install)

# stdio (local clients)
pnpm start:stdio
Point any HTTP MCP client at Agentailor's hosted endpoint:

# HTTP on PORT (default 3000)
pnpm start:http
```
https://<agentailor-mcp-host>/mcp
```

Dev mode with reload: `pnpm dev:stdio` or `pnpm dev:http`.
For a client that speaks streamable HTTP, that URL is all you need. Nothing to install or run.

## Use with Claude Desktop (stdio via Docker)
> The final hosted URL is published on [agentailor.com](https://agentailor.com).

Build the image, then point Claude Desktop at it.
### npx (local stdio)

```bash
docker build -t agentailor-mcp .
```

In your Claude Desktop config (`claude_desktop_config.json`):
Run the server as a local command — ideal for Claude Desktop. In `claude_desktop_config.json`:

```json
{
"mcpServers": {
"agentailor": {
"command": "docker",
"args": ["run", "-i", "--rm", "agentailor-mcp"]
"command": "npx",
"args": ["-y", "@agentailor/mcp"]
}
}
}
```

To raise the GitHub rate limit for the repo tools, pass a token:
The repo tools read **public** GitHub data, so no token is required. If you hit GitHub's unauthenticated rate limit (60 requests/hour), pass a token to raise it to 5,000/hour:

```json
{
"mcpServers": {
"agentailor": {
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "GITHUB_TOKEN", "agentailor-mcp"],
"command": "npx",
"args": ["-y", "@agentailor/mcp"],
"env": { "GITHUB_TOKEN": "ghp_your_token" }
}
}
}
```

## Self-host over HTTP
## Designed to the "Writing Effective Tools for AI Agents" playbook

These tools follow the five principles from Agentailor's [Writing Effective Tools for AI Agents](https://blog.agentailor.com/posts/writing-tools-for-ai-agents) — here's exactly how:

| Principle | How this server applies it |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **1. Strategic selection** | One `agentailor_search_articles` covers discovery by topic, by tag, and guides-only — instead of four fragmented search tools. Tools map to how you actually work, not to endpoints. |
| **2. Clear namespacing** | Every tool is prefixed `agentailor_`, so it never collides with other MCP servers on your client. |
| **3. Meaningful context** | Read tools take `response_format: "concise" \| "detailed"`. Results carry titles, tags, and summaries you can reason on — not bare IDs. |
| **4. Token efficiency + guiding errors** | Search defaults to 20 results with an overflow hint that tells the agent how to narrow. A GitHub rate-limit returns "set a `GITHUB_TOKEN`" guidance, not a raw 403. |
| **5. Descriptions as prompts** | Each tool description includes a **When to use** section with example calls, and every parameter documents its shape with an example. |

## Deploy your own HTTP instance

The HTTP transport serves the MCP endpoint at `POST /mcp` and a `GET /health` check.

### Native Node host

On any platform that runs a Node web service from this repo:

- **Build command:** `pnpm install --frozen-lockfile && pnpm build`
- **Start command:** `pnpm start:http`
- **Health check path:** `/health`
- **Environment:**
- `GITHUB_TOKEN` — a token with public read access (recommended on a shared/hosted instance; raises the GitHub limit to 5,000/hour).
- `PORT` — most hosts inject this automatically; the server reads it. Defaults to `3000` if unset.

Node 20 is pinned via `engines` and `.node-version`.

### Docker

A `Dockerfile` is included for running the server on any container host. The `TRANSPORT` build arg selects the entrypoint:

```bash
docker build --build-arg TRANSPORT=http -t agentailor-mcp-http .
docker run -p 3000:3000 agentailor-mcp-http
docker run -p 3000:3000 -e GITHUB_TOKEN=ghp_your_token agentailor-mcp-http
```

The MCP endpoint is `POST /mcp`; connect any HTTP MCP client to `http://<host>:3000/mcp`. A `GET /health` endpoint returns `200` for container health checks.
Connect any HTTP MCP client to `http://<host>:3000/mcp`.

## Configuration

| Variable | Required | Purpose |
| --------------- | -------- | --------------------------------------------------------------------------------------------------------------- |
| `PORT` | No | HTTP transport port (default `3000`). Ignored by stdio. |
| `GITHUB_TOKEN` | No | Raises the GitHub API limit for the repo tools from 60/hr to 5,000/hr. Any token with public read access works. |
| `BLOG_BASE_URL` | No | Override the blog origin (for local/staging testing). |
| Variable | Required | Purpose |
| --------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PORT` | No | HTTP transport port (default `3000`). Injected by most hosts. Ignored by stdio. |
| `GITHUB_TOKEN` | No | Raises the GitHub API limit for the repo tools from 60/hr to 5,000/hr. Any token with public read access works. Recommended on a hosted/shared instance. |
| `BLOG_BASE_URL` | No | Override the blog origin (for local/staging testing). |

No credentials are required to run — all exposed data is public.

## Develop and test
## Develop locally (contributors)

Clone and run from source:

```bash
pnpm install
pnpm build

# stdio (local clients)
pnpm start:stdio

# HTTP on PORT (default 3000)
pnpm start:http
```

Dev mode with reload: `pnpm dev:stdio` or `pnpm dev:http`.

```bash
pnpm test # Vitest unit tests
pnpm typecheck # tsc --noEmit
pnpm format:check # Prettier check
pnpm inspect # MCP Inspector against the HTTP server
pnpm inspect:stdio # list tools over stdio
```

## Releasing

The npm package publishes via GitHub Actions when a **GitHub Release** is published:

1. Bump `version` in `package.json` (and `src/server.ts`), commit.
2. Cut a GitHub Release / tag.
3. The `publish` workflow runs the test/build gates and publishes `@agentailor/mcp` to npm (auth via the `NPM_TOKEN` repo secret).

## Learn more

- [Writing Effective Tools for AI Agents](https://blog.agentailor.com/posts/writing-tools-for-ai-agents) — the design playbook this server follows
Expand Down
28 changes: 27 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,38 @@
{
"name": "agentailor-mcp",
"name": "@agentailor/mcp",
"version": "0.1.0",
"description": "MCP server for Agentailor — search the blog and read articles and open-source repo docs over the Model Context Protocol.",
"type": "module",
"license": "Apache-2.0",
"main": "dist/index.stdio.js",
"bin": {
"agentailor-mcp": "dist/index.stdio.js"
},
"files": [
"dist"
],
"keywords": [
"mcp",
"model-context-protocol",
"agentailor",
"fastmcp",
"ai",
"llm"
],
"homepage": "https://github.com/agentailor/agentailor-mcp#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/agentailor/agentailor-mcp.git"
},
"bugs": {
"url": "https://github.com/agentailor/agentailor-mcp/issues"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "tsc -p tsconfig.build.json",
"prepublishOnly": "pnpm run build",
"dev:stdio": "tsx watch src/index.stdio.ts",
"dev:http": "tsx watch src/index.http.ts",
"start:stdio": "node dist/index.stdio.js",
Expand Down
6 changes: 3 additions & 3 deletions src/content.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** Blog post index: typed model + search/filter helpers over llms.json. */

import { LLMS_JSON_URL } from "./config.js";
import { fetchTextCached } from "./http.js";
import { fetchText } from "./http.js";

export interface Post {
title: string;
Expand All @@ -18,9 +18,9 @@ interface LlmsJson {
posts: Post[];
}

/** Fetches and parses the structured post index (cached). */
/** Fetches and parses the structured post index. */
export async function getPosts(): Promise<Post[]> {
const raw = await fetchTextCached(LLMS_JSON_URL);
const raw = await fetchText(LLMS_JSON_URL);
const parsed = JSON.parse(raw) as LlmsJson;
return parsed.posts ?? [];
}
Expand Down
Loading
Loading