Spark Feed MCP
by Trey-Gaines
README.md
# Spark Feed MCP
Spark Feed MCP is an OAuth-protected remote MCP server for Cloudflare Workers.
It exposes two server-controlled tools:
- `get_videos` reads the feeds hardcoded in `VIDEO_OPML`.
- `get_articles` reads the feeds hardcoded in `ARTICLE_OPML`.
Both tools require a `date` argument in `MM/DD/YYYY` format and return entries
published during that UTC calendar day. Article results additionally include
the first 30 Unicode characters of the cleaned article body.
## Project files
| File | Purpose |
| --- | --- |
| `index.js` | Cloudflare Worker, MCP JSON-RPC endpoint, OAuth consent flow, OPML configuration, feed fetching, XML parsing, normalization, and date filtering. |
| `package.json` | Dependency versions and Wrangler commands. |
| `package-lock.json` | Reproducible npm dependency tree. |
| `wrangler.jsonc` | Cloudflare Worker entry point and required OAuth KV binding. |
| `.gitignore` | Prevents dependencies, local secrets, and generated output from being committed. |
## What `index.js` does
### Hardcoded feeds
`VIDEO_OPML` and `ARTICLE_OPML` are XML string constants near the beginning of
`index.js`. MCP callers cannot provide a feed URL, which keeps outbound requests
limited to the feeds you configure on the server.
Each OPML list can contain one or more nested `outline` elements:
```xml
<outline
text="Example feed"
type="rss"
xmlUrl="https://example.com/feed.xml"
/>
```
If a feed URL contains query parameters, XML-escape ampersands:
```xml
xmlUrl="https://example.com/feed.xml?topic=one&format=rss"
```
### MCP tools
Both tools take the same required input:
```json
{
"date": "12/12/2001"
}
```
`get_videos` returns:
```json
{
"items": [
{
"title": "Example video",
"link": "https://example.com/video",
"date": "2001-12-12T17:00:00.000Z"
}
]
}
```
`get_articles` returns:
```json
{
"items": [
{
"title": "Example article",
"link": "https://example.com/article",
"date": "2001-12-12T18:00:00.000Z",
"body": "The first thirty characters..."
}
]
}
```
The date filter includes timestamps greater than or equal to midnight UTC and
strictly earlier than midnight UTC on the following day. For `12/12/2001`, that
means:
```text
2001-12-12T00:00:00.000Z <= timestamp < 2001-12-13T00:00:00.000Z
```
Entries with missing or unparseable publication dates are excluded.
### Feed parsing
The Worker uses `XMLParser` from `fast-xml-parser`. It supports common:
- RSS 2.0 `channel.item` feeds.
- Atom `feed.entry` feeds.
- RDF/RSS 1.0 feeds.
It recognizes common title, link, date, description, summary, and content
fields. Dates are normalized to ISO 8601, duplicate entries are removed, and
results are sorted newest first. HTML is removed from article bodies before the
30-character preview is created.
If one configured feed fails, the Worker logs that failure and still returns
results from the remaining feeds. The tool returns an error only when every
configured feed fails.
### OAuth
`OAuthProvider` protects `/mcp` and supplies OAuth discovery, client
registration, authorization-code exchange, token refresh, revocation, bearer
validation, and OAuth storage.
The Worker implements its own `/authorize` consent page. Authorization requires
the server-side `OAUTH_ADMIN_PASSWORD` secret. The secret is never placed in
source control or `wrangler.jsonc`.
The public endpoints are:
| Path | Purpose |
| --- | --- |
| `/` | Basic service information. |
| `/mcp` | OAuth-protected MCP endpoint. |
| `/authorize` | Password-protected consent page. |
| `/token` | OAuth token endpoint. |
| `/register` | Dynamic client registration endpoint. |
## Prerequisites
- A Cloudflare account with Workers and Workers KV available.
- Node.js 22 or newer.
- npm.
Check your versions:
```bash
node --version
npm --version
```
## Setup
### 1. Install dependencies
From this project directory:
```bash
npm ci
```
`npm ci` uses the included lockfile and installs the exact tested versions.
### 2. Sign in to Cloudflare
```bash
npx wrangler login
```
Complete the authorization page Wrangler opens.
### 3. Create the OAuth KV namespace
```bash
npx wrangler kv namespace create OAUTH_KV
```
Copy the namespace ID printed by Wrangler. Open `wrangler.jsonc` and replace:
```text
REPLACE_WITH_YOUR_OAUTH_KV_NAMESPACE_ID
```
with that ID. The binding name must remain exactly `OAUTH_KV` because the OAuth
provider uses it for clients, grants, codes, access tokens, and refresh tokens.
### 4. Configure the Worker URL
Near the beginning of `index.js`, set `PUBLIC_ORIGIN` to the exact public origin
that Cloudflare will assign to this Worker:
```js
const PUBLIC_ORIGIN =
"https://spark-feed-mcp.YOUR-WORKERS-SUBDOMAIN.workers.dev";
```
Do not include a trailing slash. The value must match the deployed Worker URL
because it is used in OAuth resource and authorization-server metadata.
### 5. Configure the feeds
Replace the example `xmlUrl` in `VIDEO_OPML` with your video feed URL or URLs.
Replace the example `xmlUrl` in `ARTICLE_OPML` with your article feed URL or
URLs.
Example with multiple feeds:
```xml
<body>
<outline text="Feed one" type="rss" xmlUrl="https://one.example/feed.xml" />
<outline text="Feed two" type="rss" xmlUrl="https://two.example/atom.xml" />
</body>
```
Do not leave the `example.com` placeholder feeds in production.
### 6. Create the production authorization password
```bash
npx wrangler secret put OAUTH_ADMIN_PASSWORD
```
Enter a long, unique password at the prompt. Do not add this password to
`wrangler.jsonc`, `package.json`, or Git.
### 7. Validate the build
```bash
npm run check
```
This performs a Wrangler dry-run bundle without deploying.
### 8. Deploy
```bash
npm run deploy
```
After deployment, confirm the printed URL exactly matches `PUBLIC_ORIGIN`. If
it differs, correct `PUBLIC_ORIGIN` and deploy again.
### 9. Confirm the service
Open the Worker root URL or run:
```bash
curl https://spark-feed-mcp.YOUR-WORKERS-SUBDOMAIN.workers.dev/
```
The JSON response should list `get_videos`, `get_articles`, and the OAuth
endpoints. An unauthenticated request to `/mcp` should receive an OAuth bearer
challenge rather than tool data.
### 10. Connect Gemini
In the Gemini agent configuration that accepts a remote MCP server:
1. Use `https://spark-feed-mcp.YOUR-WORKERS-SUBDOMAIN.workers.dev/mcp` as the
MCP server URL.
2. Select OAuth authorization if the interface asks for an authentication
method.
3. Allow the client-registration and authorization flow to open.
4. Enter `OAUTH_ADMIN_PASSWORD` on the consent page and approve access.
5. Verify that Gemini discovers `get_videos` and `get_articles`.
The exact Gemini controls can vary by product surface, but the MCP URL always
ends in `/mcp`; the root Worker URL is not the MCP endpoint.
## Local development
Create a `.dev.vars` file for local-only secrets:
```dotenv
OAUTH_ADMIN_PASSWORD=replace-with-a-local-test-password
```
Then run:
```bash
npm run dev
```
`.dev.vars` is ignored by Git. Never commit real production credentials.
Local OAuth redirects still depend on `PUBLIC_ORIGIN`. For a complete local
OAuth test, temporarily use the local origin Wrangler prints, then restore the
production origin before deployment.
## Tool behavior and limits
- Date input must be a real calendar date in zero-padded `MM/DD/YYYY` form.
- Filtering uses UTC, regardless of the offset originally present in the feed.
- Feed requests time out after 10 seconds.
- Each feed is limited to 2 MB.
- At most four feeds are requested concurrently.
- Feed URLs must use HTTP or HTTPS.
- Results contain no feed metadata beyond the requested item fields.
- Article body previews contain at most 30 Unicode characters and no ellipsis
is appended.
## Troubleshooting
### `None of the configured feeds could be read`
- Confirm that the placeholder `example.com` URLs were replaced.
- Open each feed URL directly and verify it returns RSS, Atom, or RDF XML.
- Check Worker logs for the URL and HTTP status of the failed feed.
### OAuth or authorization fails
- Confirm `OAUTH_KV` exists in `wrangler.jsonc` and contains the correct
namespace ID.
- Confirm `OAUTH_ADMIN_PASSWORD` was created with `wrangler secret put`.
- Confirm `PUBLIC_ORIGIN` exactly matches the deployed HTTPS origin.
- Use the `/mcp` URL in Gemini, not `/` or `/authorize`.
### The tools return an empty `items` array
- Confirm the requested date is in UTC and in `MM/DD/YYYY` format.
- Confirm the feed entries contain a parseable publication or update date.
- Remember that an entry displayed as December 12 in a negative UTC offset may
fall on December 13 after conversion to UTC.
### Article bodies are empty
The feed must provide article text in a common field such as
`content:encoded`, `content`, `summary`, or `description`. Some feeds expose
only titles and links.
## Updating dependencies
The dependency versions are intentionally pinned. To update them later:
```bash
npm outdated
npm update
npm run check
```
Commit both `package.json` and `package-lock.json` after testing an update.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing