From 82ca5a67a237bf1400b1a81de998c9e2d744a019 Mon Sep 17 00:00:00 2001 From: Ali Ibrahim Jr <48456829+IBJunior@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:43:46 +0200 Subject: [PATCH 1/3] feat: publish as @agentailor/mcp (npx) and add HTTP deploy path - Add npm bin/publishConfig and a Node shebang for `npx @agentailor/mcp` - Rework README/Dockerfile around npx + hosted HTTP (Render native Node) - Remove the in-memory fetch cache; rename fetchTextCached to fetchText - Add CI and npm publish GitHub Actions workflows Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 29 ++++++++ .github/workflows/publish.yml | 34 +++++++++ .nvmrc | 1 + CLAUDE.md | 19 +++-- Dockerfile | 6 +- README.md | 126 ++++++++++++++++++++++------------ package.json | 28 +++++++- src/content.ts | 6 +- src/http.ts | 24 ++----- src/index.stdio.ts | 1 + src/tools/blog.ts | 6 +- src/tools/repos.ts | 6 +- test/http.test.ts | 33 ++++----- 13 files changed, 220 insertions(+), 99 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .nvmrc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..386d7fe --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +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 + with: + version: 10.33.0 + + - 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 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..4ecbe57 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,34 @@ +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 + with: + version: 10.33.0 + + - 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 }} diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/CLAUDE.md b/CLAUDE.md index ca17b69..de69928 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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** — Agentailor runs the HTTP transport on **Render as a native Node web service** (not Docker): build `pnpm install --frozen-lockfile && pnpm build`, start `pnpm start:http`. `GITHUB_TOKEN` is a Render secret; `PORT` is injected by Render; 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 — **not** the Render path. + +**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 @@ -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`) @@ -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. @@ -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 — **not** the hosted Render deployment (Render uses the native Node runtime). `TRANSPORT` build ARG selects the entrypoint: ```bash docker build -t agentailor-mcp . # stdio (default) diff --git a/Dockerfile b/Dockerfile index d98a0f8..49e0f87 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,8 @@ -# Multi-stage build. TRANSPORT selects the entrypoint: +# Multi-stage build for running the server locally or on any container host. +# (The hosted deployment on Render uses the native Node runtime, not this image.) +# 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 diff --git a/README.md b/README.md index bd5d8d8..dadbaf1 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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:///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. + +### On Render (native Node web service — reference setup) + +Create a **Web Service** from this repo with the **Node** runtime (not Docker): + +- **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` — injected by Render automatically; the server reads it. No need to set it. + +Node 20 is pinned via `engines` and `.node-version`. + +### Anywhere else (Docker) + +A `Dockerfile` is included for running the server outside Render — locally or 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://:3000/mcp`. A `GET /health` endpoint returns `200` for container health checks. +Connect any HTTP MCP client to `http://: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 the host on Render. 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 diff --git a/package.json b/package.json index f41d266..0e79f4f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/content.ts b/src/content.ts index 0b6e1af..25b2f66 100644 --- a/src/content.ts +++ b/src/content.ts @@ -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; @@ -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 { - const raw = await fetchTextCached(LLMS_JSON_URL); + const raw = await fetchText(LLMS_JSON_URL); const parsed = JSON.parse(raw) as LlmsJson; return parsed.posts ?? []; } diff --git a/src/http.ts b/src/http.ts index a2bc0a7..f99fbb1 100644 --- a/src/http.ts +++ b/src/http.ts @@ -1,4 +1,4 @@ -/** Fetch helpers: an in-memory cache and GitHub rate-limit handling. */ +/** Fetch helpers: a text fetch with GitHub rate-limit handling. */ /** Raised when GitHub's rate limit is hit; carries an agent-actionable message. */ export class RateLimitError extends Error { @@ -8,10 +8,6 @@ export class RateLimitError extends Error { } } -// Process-lifetime cache. Data is public and changes rarely, so caching keeps -// us well under GitHub's unauthenticated 60 req/hr/IP budget. -const cache = new Map(); - /** Adds an Authorization header only when the user supplied a token. */ export function githubHeaders(): HeadersInit { const token = process.env.GITHUB_TOKEN; @@ -33,16 +29,13 @@ const RATE_LIMIT_HINT = "Set a GITHUB_TOKEN environment variable to raise the limit to 5,000/hour, then retry."; /** - * Cached text fetch. On GitHub rate-limit responses, throws a RateLimitError - * with an actionable message instead of surfacing a raw 403. + * Text fetch. On GitHub rate-limit responses, throws a RateLimitError with an + * actionable message instead of surfacing a raw 403. */ -export async function fetchTextCached( +export async function fetchText( url: string, init?: RequestInit ): Promise { - const cached = cache.get(url); - if (cached !== undefined) return cached; - const res = await fetch(url, init); if (isRateLimited(res)) throw new RateLimitError(RATE_LIMIT_HINT); @@ -51,9 +44,7 @@ export async function fetchTextCached( throw new Error(`Request failed: ${res.status} ${res.statusText}`); } - const text = await res.text(); - cache.set(url, text); - return text; + return res.text(); } /** Raised on 404 so callers can return a friendly "not found" tool result. */ @@ -63,8 +54,3 @@ export class NotFoundError extends Error { this.name = "NotFoundError"; } } - -/** Test-only: clear the cache between cases. */ -export function _clearCache(): void { - cache.clear(); -} diff --git a/src/index.stdio.ts b/src/index.stdio.ts index 020b65f..242e1dd 100644 --- a/src/index.stdio.ts +++ b/src/index.stdio.ts @@ -1,3 +1,4 @@ +#!/usr/bin/env node import "dotenv/config"; import { server } from "./server.js"; diff --git a/src/tools/blog.ts b/src/tools/blog.ts index 28ffe36..d64f4e5 100644 --- a/src/tools/blog.ts +++ b/src/tools/blog.ts @@ -7,7 +7,7 @@ import { } from "../config.js"; import { getPosts, searchPosts } from "../content.js"; import { formatSearchResults } from "../format.js"; -import { fetchTextCached, NotFoundError } from "../http.js"; +import { fetchText, NotFoundError } from "../http.js"; import { isBlogArticleUrl } from "../validate.js"; /** Registers the blog content tools on the given server. */ @@ -22,7 +22,7 @@ export function registerBlogTools(server: FastMCP): void { - Prefer agentailor_search_articles when the user names a topic — it returns a filtered, ranked shortlist instead of the whole map.`, parameters: z.object({}), execute: async () => { - return await fetchTextCached(BLOG_INDEX_URL); + return await fetchText(BLOG_INDEX_URL); }, }); @@ -107,7 +107,7 @@ Pass the .md URL from the search results (e.g. https://blog.agentailor.com/posts ); } try { - return await fetchTextCached(url); + return await fetchText(url); } catch (err) { if (err instanceof NotFoundError) { throw new UserError( diff --git a/src/tools/repos.ts b/src/tools/repos.ts index 645c013..497c7ea 100644 --- a/src/tools/repos.ts +++ b/src/tools/repos.ts @@ -2,7 +2,7 @@ import { FastMCP, UserError } from "fastmcp"; import { z } from "zod"; import { GITHUB_ORG } from "../config.js"; import { - fetchTextCached, + fetchText, githubHeaders, NotFoundError, RateLimitError, @@ -37,7 +37,7 @@ export function registerRepoTools(server: FastMCP): void { }), execute: async ({ response_format }) => { const raw = await withRateLimitHint(() => - fetchTextCached( + fetchText( `https://api.github.com/orgs/${GITHUB_ORG}/repos?type=public&per_page=100&sort=updated`, { headers: githubHeaders() } ) @@ -95,7 +95,7 @@ Only README.md and docs/*.md paths are allowed.`, const url = `https://raw.githubusercontent.com/${GITHUB_ORG}/${repo}/HEAD/${normalized}`; try { return await withRateLimitHint(() => - fetchTextCached(url, { headers: githubHeaders() }) + fetchText(url, { headers: githubHeaders() }) ); } catch (err) { if (err instanceof NotFoundError) { diff --git a/test/http.test.ts b/test/http.test.ts index f92d49b..1f991d5 100644 --- a/test/http.test.ts +++ b/test/http.test.ts @@ -1,10 +1,5 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { - _clearCache, - fetchTextCached, - NotFoundError, - RateLimitError, -} from "../src/http.js"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { fetchText, NotFoundError, RateLimitError } from "../src/http.js"; function mockResponse( body: string, @@ -16,18 +11,18 @@ function mockResponse( }); } -describe("fetchTextCached", () => { - beforeEach(() => _clearCache()); +describe("fetchText", () => { afterEach(() => vi.restoreAllMocks()); - it("returns body text on success and caches it", async () => { + it("returns body text on success and does not cache", async () => { + // Fresh Response per call: a body can only be read once. const spy = vi .spyOn(globalThis, "fetch") - .mockResolvedValue(mockResponse("hello")); + .mockImplementation(async () => mockResponse("hello")); - expect(await fetchTextCached("https://x.test/a")).toBe("hello"); - expect(await fetchTextCached("https://x.test/a")).toBe("hello"); - expect(spy).toHaveBeenCalledTimes(1); // second call served from cache + expect(await fetchText("https://x.test/a")).toBe("hello"); + expect(await fetchText("https://x.test/a")).toBe("hello"); + expect(spy).toHaveBeenCalledTimes(2); // no cache — each call hits the network }); it("throws RateLimitError on 403 with x-ratelimit-remaining: 0", async () => { @@ -37,10 +32,10 @@ describe("fetchTextCached", () => { headers: { "x-ratelimit-remaining": "0" }, }) ); - await expect(fetchTextCached("https://x.test/rl")).rejects.toBeInstanceOf( + await expect(fetchText("https://x.test/rl")).rejects.toBeInstanceOf( RateLimitError ); - await expect(fetchTextCached("https://x.test/rl")).rejects.toThrow( + await expect(fetchText("https://x.test/rl")).rejects.toThrow( /GITHUB_TOKEN/ ); }); @@ -49,7 +44,7 @@ describe("fetchTextCached", () => { vi.spyOn(globalThis, "fetch").mockResolvedValue( mockResponse("slow down", { status: 429 }) ); - await expect(fetchTextCached("https://x.test/429")).rejects.toBeInstanceOf( + await expect(fetchText("https://x.test/429")).rejects.toBeInstanceOf( RateLimitError ); }); @@ -61,7 +56,7 @@ describe("fetchTextCached", () => { headers: { "x-ratelimit-remaining": "42" }, }) ); - const err = await fetchTextCached("https://x.test/403").catch((e) => e); + const err = await fetchText("https://x.test/403").catch((e) => e); expect(err).not.toBeInstanceOf(RateLimitError); expect(err).toBeInstanceOf(Error); }); @@ -70,7 +65,7 @@ describe("fetchTextCached", () => { vi.spyOn(globalThis, "fetch").mockResolvedValue( mockResponse("nope", { status: 404 }) ); - await expect(fetchTextCached("https://x.test/404")).rejects.toBeInstanceOf( + await expect(fetchText("https://x.test/404")).rejects.toBeInstanceOf( NotFoundError ); }); From 66fa066b518b0075ec223220aec0d79460ff7f7e Mon Sep 17 00:00:00 2001 From: Ali Ibrahim Jr <48456829+IBJunior@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:52:29 +0200 Subject: [PATCH 2/3] docs: keep hosting details generic Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 6 +++--- Dockerfile | 1 - README.md | 12 ++++++------ 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index de69928..b724b42 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,9 +13,9 @@ Guidance for Claude Code (claude.ai/code) when working in this repository. 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** — Agentailor runs the HTTP transport on **Render as a native Node web service** (not Docker): build `pnpm install --frozen-lockfile && pnpm build`, start `pnpm start:http`. `GITHUB_TOKEN` is a Render secret; `PORT` is injected by Render; health check is `/health`. Node 20 pinned via `engines` + `.node-version`. +- **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 — **not** the Render path. +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. @@ -88,7 +88,7 @@ Vitest, offline and deterministic (network is mocked via `vi.spyOn(globalThis, " ## Docker -The `Dockerfile` is for local stdio and self-hosting anywhere — **not** the hosted Render deployment (Render uses the native Node runtime). `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) diff --git a/Dockerfile b/Dockerfile index 49e0f87..e4b96ea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,4 @@ # Multi-stage build for running the server locally or on any container host. -# (The hosted deployment on Render uses the native Node runtime, not this image.) # TRANSPORT selects the entrypoint: # stdio (default) — local clients (Claude Desktop) via `docker run -i` # http — self-host anywhere on PORT (default 3000) diff --git a/README.md b/README.md index dadbaf1..b3653f6 100644 --- a/README.md +++ b/README.md @@ -80,22 +80,22 @@ These tools follow the five principles from Agentailor's [Writing Effective Tool The HTTP transport serves the MCP endpoint at `POST /mcp` and a `GET /health` check. -### On Render (native Node web service — reference setup) +### Native Node host -Create a **Web Service** from this repo with the **Node** runtime (not Docker): +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` — injected by Render automatically; the server reads it. No need to set it. + - `PORT` — most hosts inject this automatically; the server reads it. Defaults to `3000` if unset. Node 20 is pinned via `engines` and `.node-version`. -### Anywhere else (Docker) +### Docker -A `Dockerfile` is included for running the server outside Render — locally or on any container host. The `TRANSPORT` build arg selects the entrypoint: +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 . @@ -108,7 +108,7 @@ Connect any HTTP MCP client to `http://:3000/mcp`. | Variable | Required | Purpose | | --------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `PORT` | No | HTTP transport port (default `3000`). Injected by the host on Render. Ignored by stdio. | +| `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). | From 62a8d9a3624a220022854f618b0c17ab986aafa7 Mon Sep 17 00:00:00 2001 From: Ali Ibrahim Jr <48456829+IBJunior@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:57:37 +0200 Subject: [PATCH 3/3] ci: let pnpm/action-setup read the version from packageManager --- .github/workflows/ci.yml | 2 -- .github/workflows/publish.yml | 2 -- 2 files changed, 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 386d7fe..cde01c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,8 +13,6 @@ jobs: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - with: - version: 10.33.0 - uses: actions/setup-node@v4 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4ecbe57..cd12c3e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,8 +13,6 @@ jobs: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - with: - version: 10.33.0 - uses: actions/setup-node@v4 with: