Skip to content
Live demo

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.

https://beta.sparkfeed.dev/api/mcp

Transport 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.

claude mcp add --transport http sparkfeed https://beta.sparkfeed.dev/api/mcp \
--header "Authorization: Bearer sfk_live_YOUR_KEY"

Check it registered:

claude mcp list

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.

ToolPurpose
get_workspace_infoCounts, plan, what the key may do, and the limits in force
list_foldersThe workspace tree with per-folder feed counts
list_feedsSources, optionally filtered to one folder
search_articlesSearch, or omit the query for newest-first
get_articleThe full text of one article, as markdown
set_favoriteFavorite or unfavorite up to 100 articles
mark_readMark articles read or unread

Two rules keep an agent’s context from filling up with feed data:

  1. List tools return snippets, never article bodies. search_articles gives you titles, sources, dates, and roughly 300 characters of summary.
  2. Full text is one article at a time. get_article is 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.

Four calls, roughly 4KB total:

  1. list_folders to see what exists
  2. search_articles with a folder_id and since to narrow to what is new
  3. get_article on the one that looks worth reading
  4. set_favorite to keep it

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.

ParameterNotes
queryFree text over title and summary. Omit for newest-first.
folder_id, feed_idScope to one folder or feed
since, untilISO dates, or relative shorthand like 7d or 24h
favorites_only, unread_onlyBooleans
limitDefault 20, maximum 50
cursorThe 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.

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.

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.

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.

ScopeGrants
mcpAccess to the endpoint. Every key has it.
workspace:readget_workspace_info, list_folders, list_feeds
articles:readsearch_articles, get_article
articles:writeset_favorite, mark_read
Authenticated keyDemo key
Requests per minute12060, per client
Response ceiling50KB50KB
search_articles limit50 max50 max
get_article length80,000 characters max80,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.

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 scheme
Using custom headers: {"Authorization":"Bearer sfk_live_…"} ← right

Then 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.

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-resource
https://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.

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.