MCP Server
Sparkfeed speaks the Model Context Protocol, so an AI agent can work directly against the feeds you already curate. Ask Claude what shipped in your AI research folder this week and it queries your workspace instead of searching the open web.
The point is the curation. An agent pointed at the internet gets whatever ranks; an agent pointed at your workspace gets the sources you chose to trust.
Endpoint
Section titled “Endpoint”https://beta.sparkfeed.dev/api/mcpTransport is Streamable HTTP. Authentication is a bearer token, which you create on the Developer → API keys page inside the app. See API keys for how to mint one.
Self-hosting? Use your own origin with the same path, for example https://feeds.example.com/api/mcp.
Connect a client
Section titled “Connect a client”claude mcp add --transport http sparkfeed https://beta.sparkfeed.dev/api/mcp \ --header "Authorization: Bearer sfk_live_YOUR_KEY"Check it registered:
claude mcp listAdd to your MCP config file:
{ "mcpServers": { "sparkfeed": { "type": "http", "url": "https://beta.sparkfeed.dev/api/mcp", "headers": { "Authorization": "Bearer sfk_live_YOUR_KEY" } } }}Claude Desktop connects natively. Settings → Connectors → Add custom connector, point it at the endpoint, and set one request header:
| Field | Value |
|---|---|
| URL | https://beta.sparkfeed.dev/api/mcp |
| Header name | Authorization |
| Header value | Bearer sfk_live_YOUR_KEY |
Include the word Bearer and the space after it. The value is the whole header, not just the key.
Bridging with mcp-remote
{ "mcpServers": { "sparkfeed": { "command": "npx", "args": [ "-y", "mcp-remote", "https://beta.sparkfeed.dev/api/mcp", "--header", "Authorization:${SPARKFEED_AUTH}" ], "env": { "SPARKFEED_AUTH": "Bearer sfk_live_YOUR_KEY" } } }}Copy that shape exactly. Claude Desktop and Cursor do not escape spaces when they invoke npx, so any argument containing one arrives mangled — which is why there is no space after the colon in Authorization:${SPARKFEED_AUTH}, and why the token lives in env where its space is safe.
mcp-remote is an OAuth bridge. If the endpoint rejects your key it will abandon the token and start an OAuth handshake that Sparkfeed does not implement, and the resulting error will talk about registration rather than about your key. Treat any mention of OAuth in its output as “the key did not work”.
Try it without an account
Section titled “Try it without an account”The demo workspace exposes a public, read-only endpoint. No sign-up:
claude mcp add --transport http sparkfeed-demo https://demo.sparkfeed.dev/api/mcp \ --header "Authorization: Bearer sfk_demo_public"It serves the nine seeded demo feeds, is rate limited because everyone shares it, and only ever reads. Write tools are not registered at all, so an agent connected to it will not even see them.
Read tools are always available. Write tools appear only when the key carries the articles:write scope.
| Tool | Purpose |
|---|---|
get_workspace_info | Counts, plan, what the key may do, and the limits in force |
list_folders | The workspace tree with per-folder feed counts |
list_feeds | Sources, optionally filtered to one folder |
search_articles | Search, or omit the query for newest-first |
get_article | The full text of one article, as markdown |
set_favorite | Favorite or unfavorite up to 100 articles |
mark_read | Mark articles read or unread |
How responses stay small
Section titled “How responses stay small”Two rules keep an agent’s context from filling up with feed data:
- List tools return snippets, never article bodies.
search_articlesgives you titles, sources, dates, and roughly 300 characters of summary. - Full text is one article at a time.
get_articleis the only way to read a whole post, and it takes a single id. The expensive call is deliberately opt-in and cannot be batched.
Every response is also capped at 50KB. If a page would exceed it, the payload is cut at an item boundary and comes back with truncated: true plus a next_cursor to continue from.
A typical session
Section titled “A typical session”Four calls, roughly 4KB total:
list_foldersto see what existssearch_articleswith afolder_idandsinceto narrow to what is newget_articleon the one that looks worth readingset_favoriteto keep it
Searching
Section titled “Searching”search_articles handles both “find X” and “what is new”, because query is optional. Omit it and you get the most recent articles instead, which is usually what you want.
| Parameter | Notes |
|---|---|
query | Free text over title and summary. Omit for newest-first. |
folder_id, feed_id | Scope to one folder or feed |
since, until | ISO dates, or relative shorthand like 7d or 24h |
favorites_only, unread_only | Booleans |
limit | Default 20, maximum 50 |
cursor | The next_cursor from a previous response |
Results carry has_full_text, which tells the agent whether get_article will be instant (already cached) or will need to fetch the page first.
Reading an article
Section titled “Reading an article”get_article returns markdown by default, because that is what a language model reads most cheaply. Pass format: "text" or format: "html" if you need something else.
It resolves in three steps: cached text, then a live fetch with readability extraction (cached for next time), then the RSS summary as a last resort. The source_quality field tells you which one you got, so rss_description is a signal that the article’s full text was not reachable.
Long articles come back in chunks. When truncated is true, call again with the returned next_offset.
Identifiers
Section titled “Identifiers”Ids are prefixed by type: fld_ folders, fed_ feeds, art_ articles. Pass them back exactly as received. The prefix is load-bearing rather than decorative, since articles and crawled pages live in separate tables and the prefix is what tells them apart.
Scopes
Section titled “Scopes”A key carries scopes, and tools are registered per scope. A read-only key does not merely get refused when it calls a write tool: the tool is not in its list at all.
| Scope | Grants |
|---|---|
mcp | Access to the endpoint. Every key has it. |
workspace:read | get_workspace_info, list_folders, list_feeds |
articles:read | search_articles, get_article |
articles:write | set_favorite, mark_read |
Limits
Section titled “Limits”| Authenticated key | Demo key | |
|---|---|---|
| Requests per minute | 120 | 60, per client |
| Response ceiling | 50KB | 50KB |
search_articles limit | 50 max | 50 max |
get_article length | 80,000 characters max | 80,000 characters max |
Exceeding the rate limit returns 429 with a retry_after_ms field. A limit above the maximum is clamped rather than rejected.
Troubleshooting
Section titled “Troubleshooting”Your client mentions OAuth, discovery, or registration
Sparkfeed does not implement OAuth. Any client talking about it has been refused a bearer token and fallen back to a handshake — so this is almost always a key problem wearing a disguise. Nine times in ten the Authorization value lost its Bearer prefix.
Check what your client actually sent. mcp-remote logs it on startup:
Using custom headers: {"Authorization":"sfk_live_…"} ← wrong, no schemeUsing custom headers: {"Authorization":"Bearer sfk_live_…"} ← rightThen confirm the key itself against the endpoint:
curl -s -o /dev/null -w '%{http_code}\n' \ -X POST https://beta.sparkfeed.dev/api/mcp \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -H 'Authorization: Bearer sfk_live_YOUR_KEY' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'200 means the key is fine and the problem is in how your client passes it. 401 means the key is wrong, revoked, or missing its scheme.
401 with WWW-Authenticate: Bearer
The key is missing, revoked, or malformed. Confirm the header reads Authorization: Bearer sfk_live_…. The challenge carries a resource_metadata URL pointing at this endpoint’s RFC 9728 description.
403 insufficient_scope
The key is valid but lacks a scope the endpoint requires. Mint a new one.
The agent cannot see set_favorite or mark_read
Expected on a read-only key, and on the demo endpoint. Those tools are only registered when the key carries articles:write.
get_article returns very little, with source_quality: "rss_description"
The article’s full text was not reachable, so the RSS summary was served instead. On the demo endpoint this is expected for uncached articles: live fetching is disabled there on purpose.
Discovery metadata
Section titled “Discovery metadata”Sparkfeed is an OAuth 2.0 resource server with no authorization server behind it: a key is a static bearer token, minted in the app. It still publishes the standard description of itself, at both spellings clients probe:
https://beta.sparkfeed.dev/.well-known/oauth-protected-resourcehttps://beta.sparkfeed.dev/.well-known/oauth-protected-resource/api/mcp{ "resource": "https://beta.sparkfeed.dev/api/mcp", "bearer_methods_supported": ["header"], "scopes_supported": ["mcp", "workspace:read", "articles:read", "articles:write"], "resource_name": "Sparkfeed"}There is no authorization_servers key, because there is no authorization server. /.well-known/oauth-authorization-server answers 404 on purpose — advertising endpoints that do not exist would send clients into a flow that cannot complete.
Verify a connection
Section titled “Verify a connection”npx @modelcontextprotocol/inspector --cli \ --method tools/list \ --transport http \ --server-url https://beta.sparkfeed.dev/api/mcp \ --header "Authorization: Bearer sfk_live_YOUR_KEY"A working key lists the tools available to it. That output is also the fastest way to confirm which scopes a key actually has.