Skip to main content
Glama
wiz0floyd

sn-community-mcp

by wiz0floyd
README.md
# sn-community-mcp

CLI and MCP server for searching the ServiceNow Community forums (community.servicenow.com).

## How it works

The Community runs on the Khoros/Lithium forum platform. This tenant does not expose the
public LiQL REST API (`/community/api/2.1/search` returns 404 for anonymous access). Khoros'
RSS search feed (`/community/s/<id>/rss/search`) is reachable, but **do not use it for
ranked search** — it turned out to sort by recency, not relevance, and returned off-topic,
same-day results for a query like "API Management" while the real search page returned
on-topic results. (That gap was only caught by comparing live output against the actual
search page side by side — the RSS feed still "worked" in every other sense: valid XML,
plausible-looking posts, no errors.)

Instead, this tool fetches and parses the same server-rendered HTML search-results page a
user's browser loads (`/community/forums/searchpage/tab/message?...q=<query>`) — genuinely
relevance-ranked ("Best Match" by default), and readable via a plain HTTP GET with no JS
execution or browser automation needed, since the results are already present in the raw
HTML. `full: true` does a second plain-HTTP fetch of the post's permalink page to pull the
complete body, converted to Markdown.

Because this reads undocumented page structure rather than a stable API, it's more fragile
than a real API would be — see "Design notes" below for how to notice and fix breakage.

## Install

```bash
npm install
npm run build
```

## MCP Server (Claude integration)

The MCP server exposes one tool:

| Tool | Description |
|---|---|
| `search_community` | Search the Community. Returns title, link, board, author, date, and body (snippet or full Markdown). |

### Register with Claude Code

Add to `~/.claude/settings.json`:

```json
{
  "mcpServers": {
    "sn-community": {
      "command": "node",
      "args": ["/path/to/sn-community-mcp/dist/mcp-server.js"]
    }
  }
}
```

### Register with Claude Desktop

Add the same block to your Claude Desktop config, replacing the path with the absolute path
to `dist/mcp-server.js` on your machine.

### `search_community` parameters

- `query` (string, required) — search terms.
- `count` (number, optional, default 10, max 30) — number of results. Fetched 10 at a time
  (the observed page size), so counts above 10 make additional page requests.
- `full` (boolean, optional, default `false`) — return the full post as Markdown (fetched from
  the post's permalink page) instead of a ~400-character snippet. Combine with a small `count`
  to avoid large responses and extra requests. Falls back to the snippet if the permalink
  fetch fails or doesn't have the expected content (see Design notes).

There is no `sort` parameter — no working relevance/date sort parameter was found for the
HTML search endpoint (an attempt returned HTTP 500), so results are always in the site's
default "Best Match" order.

## CLI

```bash
npx tsx src/cli.ts search "API Management"
npx tsx src/cli.ts search "API Management" --full -n 3
npx tsx src/cli.ts search "API Management" --json
```

Or after `npm run build`, use the `sn-community` bin directly.

## Development

```bash
npm run dev:mcp           # Run MCP server via tsx (no build needed)
npm test                  # Unit tests (mocked fetch, no network)
npm run test:integration  # Live smoke test against the real search page
npm run lint              # Type-check without emitting
```

Unit tests replay real captured HTML pages (`tests/fixtures/*.html`) rather than hand-written
fixtures, so selector-based parsing is verified against real markup — including a fixture
that reproduces the one observed permalink-fetch failure case (see below).

## Design notes / known limitations

- **Scraping, not a documented API.** `src/community-client.ts` selects on CSS classes like
  `.lia-message-view-message-search-item`, `h1.message-subject`, `.lia-truncated-body-container`,
  and `.lia-message-body-content` — standard Khoros/Lithium theme class names, but undocumented
  and subject to change without notice. If `search_community` starts returning empty results
  or garbled fields, the site's markup likely changed; re-run the CLI with `--json` against a
  known-good query, compare to a fresh page fetch, and update the selectors.
- **No confirmed sort/date param for the HTML endpoint.** RSS supported `sort_by`/`sort_order`,
  but replaying the same params against the HTML search page returned HTTP 500. Results are
  always in "Best Match" (relevance) order — the same default the community site's own search
  box uses.
- **`full: true` falls back to the snippet if a permalink doesn't render as expected.** Both
  forum-message (`m-p/`) and KB-article (`ta-p/`) permalinks render their full body correctly
  via a plain fetch — this was verified directly for both link styles, so it's not a
  systemic gap for either. `fetchFullBody()` still treats a missing
  `.lia-message-body-content` container as "no full body available" and returns the original
  snippet rather than an error, as defense against the case where a permalink 404s, redirects
  somewhere unexpected, or the page structure changes. `tests/fixtures/post-page-missing-body.html`
  reproduces that fallback path using a page that doesn't contain the body container (a
  malformed-URL redirect target captured during testing), not a class of URL that fails in
  normal use.
- **No `get_community_post(url)` tool.** The one exception is that `full: true` already covers
  the "get a specific post's full content" need for posts found via search; a pasted link's
  title/keywords can be searched instead of adding a second tool.
- **No Cloudflare Worker variant.** This ships as a local stdio MCP server only. The sibling
  `sn-docs` project (`../Docs CLI`) proves the Worker path
  (`WebStandardStreamableHTTPServerTransport` + `wrangler.toml`) if hosted/Claude.ai access is
  ever wanted; `cheerio` (used here) runs fine in Workers, so only the Turndown-based
  `toMarkdown()` full-body conversion would need a DOM-free port, same as `sn-docs`'s
  `toMarkdownWorker`.