RISE & AI Sweden — Public MCP

A public MCP server that indexes AI-relevant publications from RISE and AI Sweden — Sweden's two government-funded AI organizations. Provided in good faith to support the growth of the agentic AI ecosystem.

v202608211830Published August 21, 2026·Developing version, constantly in progress

Add it to your AI assistant

In most modern MCP-compatible tools, this URL is all you need. No login, no API key, no authentication.

Server URL
https://rise-ai-sweden.jardenberg.org/api/mcp
Suggested name
rise-ai-sweden
Authentication
None
2,312
Documents indexed
6,766
Searchable chunks

Where to add it

These menus change often — if something looks different, search the app's settings for “MCP”.

Claude Desktop
Settings → Developer → Edit Config, then add an entry under mcpServers with the URL above.
Claude.ai (web)
Customize → Connectors → + → Add custom connector → paste the URL.
ChatGPT (Pro/Team)
Settings → Connectors → Add → MCP server → paste the URL.
Cursor
Settings → MCP → Add new server → paste the URL.
Codex CLI / others
Add an entry to your mcpServers config pointing at the URL.

How to use it in a chat

Once added, just mention the name in your prompt. For example:

“Use rise-ai-sweden to give me the most important content related to Helsingborg.”

JSON config (for tools that need it)

{
  "mcpServers": {
    "rise-ai-sweden": {
      "transport": { "type": "http", "url": "https://rise-ai-sweden.jardenberg.org/api/mcp" }
    }
  }
}

Rate limit: 60 requests / 5 minutes per IP.

Tools exposed

Six tools, each returning JSON. Most accept a page_type filter (event · news · project · page). Examples below show the actual shape your assistant receives.

search_swedish_ai

Hybrid (semantic + lexical) search across all indexed RISE and AI Sweden content. Vector arm for topical queries; lexical arm (Swedish/English stemming + trigram fallback) for proper nouns and short keyword queries. Reciprocal-rank-fusion merge; lexical weighted 2x when query is ≤2 tokens.

Parameters
{
  query: string,                                     // 2–500 chars
  source?:    "rise" | "ai_sweden",
  lang?:      "en" | "sv",
  page_type?: "event" | "news" | "project" | "page",
  limit?:     number                                 // 1–50, default 20
}
Example response
{
  "query": "Helsingborg",
  "count": 2,
  "hybrid": { "rrfK": 60, "weights": { "vector": 1, "lexical": 2 }, "tokenCount": 1 },
  "results": [
    {
      "url": "https://www.ai.se/en/sector-initiatives-projects/.../national-city-lab",
      "title": "National City Lab",
      "source": "ai_sweden",
      "sourceName": "AI Sweden",
      "lang": "en",
      "pageType": "project",
      "score": 0.0312,
      "arms": { "vector": 0.4912, "lexical": 0.0821 },
      "snippet": "The National City Lab in Helsingborg...",
      "fetchedAt": "2026-06-10T08:14:22Z"
    },
    { "...": "..." }
  ]
}

find_mentions

Lexical 'everything that mentions X' primitive. No embeddings — stemmed tsquery + exact-substring fallback. Includes a total field that mirrors a direct count(DISTINCT id) WHERE raw_markdown ILIKE '%term%' SQL query.

Parameters
{
  term: string,                                      // 1–200 chars
  source?:    "rise" | "ai_sweden",
  lang?:      "en" | "sv",
  page_type?: "event" | "news" | "project" | "page",
  limit?:     number                                 // 1–100, default 25
}
Example response
{
  "term": "Helsingborg",
  "total": 39,
  "totalNote": "Exact-substring document count (ILIKE) matching the same filters. Reconciles with raw SQL.",
  "returned": 25,
  "results": [
    {
      "url": "https://www.ai.se/en/news/helsingborg-opens-national-city-lab",
      "title": "Helsingborg opens the National City Lab",
      "source": "ai_sweden",
      "sourceName": "AI Sweden",
      "lang": "en",
      "pageType": "news",
      "matchMode": "exact",
      "rank": 0.0912,
      "snippet": "Helsingborg opens the National City Lab...",
      "fetchedAt": "2026-06-09T07:32:10Z"
    },
    { "...": "..." }
  ]
}

list_latest

Cheap 'what's new' view. Without arguments, returns the newest documents across both sources. Pass source / page_type to slice. No snippets — lightweight metadata only.

Parameters
{
  source?:    "rise" | "ai_sweden",
  lang?:      "en" | "sv",
  page_type?: "event" | "news" | "project" | "page",
  limit?:     number                                 // 1–50, default 20
}
Example response
{
  "count": 2,
  "results": [
    {
      "url": "https://www.ai.se/en/news/...",
      "title": "New language model release",
      "source": "ai_sweden",
      "sourceName": "AI Sweden",
      "lang": "en",
      "pageType": "news",
      "publishedAt": "2026-06-09T00:00:00Z",
      "publishedAtSource": "meta",
      "sitemapLastmod": "2026-06-09T00:00:00Z",
      "fetchedAt": "2026-06-10T08:14:22Z"
    },
    { "...": "..." }
  ]
}

find_similar

Given a URL already in the index, return semantically nearest other documents. Uses the seed document's centroid embedding (average of all its chunks) so navigation chrome doesn't dominate matches. Great for 'more like this' after picking a hit. URLs are canonicalized on input, so bare-host, trailing-slash, and tracking-param variants all resolve.

Parameters
{
  url: string,             // a URL from search_swedish_ai / list_latest
  source?: "rise" | "ai_sweden",
  lang?:   "en" | "sv",
  limit?:  number          // 1–25, default 10
}
Example response
{
  "seedUrl": "https://www.ri.se/en/what-we-do/projects/edge-ai-...",
  "count": 2,
  "results": [
    {
      "url": "https://www.ri.se/en/what-we-do/projects/tinyml-...",
      "title": "TinyML for industrial sensors",
      "source": "rise",
      "sourceName": "RISE",
      "lang": "en",
      "score": 0.7912,
      "snippet": "Running compact ML models on microcontrollers...",
      "fetchedAt": "2026-06-07T11:02:08Z"
    },
    { "...": "..." }
  ]
}

get_document

Fetch full cleaned markdown for a single indexed URL. Use after search_swedish_ai to load complete context on the best hit. Body is capped at 80 000 chars — check `truncated` before assuming you have the whole document. `contentNote` / `extractionMethod` surface provenance (e.g. Firecrawl OCR for image-only PDFs, manually replaced stored copies).

Parameters
{ url: string }
Example response
{
  "url": "https://www.ai.se/en/project/...",
  "title": "Project title",
  "lang": "en",
  "pageType": "project",
  "contentType": "html",
  "source": "ai_sweden",
  "sourceName": "AI Sweden",
  "publishedAt": "2026-05-14T00:00:00Z",
  "publishedAtSource": "meta",
  "fetchedAt": "2026-06-08T09:01:44Z",
  "bytesReplacedAt": null,
  "extractionMethod": null,
  "contentNote": null,
  "content": "# Project title\n\nFull markdown body...",
  "truncated": false
}

list_sources

Discover scope and freshness: which sources are indexed, document counts, language breakdown, and the most recent fetch per source.

Parameters
{}
Example response
{
  "sources": [
    {
      "slug": "rise",
      "name": "RISE",
      "rootUrl": "https://www.ri.se",
      "documents": 412,
      "languages": { "en": 280, "sv": 132 },
      "lastUpdated": "2026-06-10T08:14:22Z"
    },
    {
      "slug": "ai_sweden",
      "name": "AI Sweden",
      "rootUrl": "https://www.ai.se",
      "documents": 388,
      "languages": { "en": 250, "sv": 138 },
      "lastUpdated": "2026-06-10T07:55:01Z"
    }
  ]
}

server_info

Server identity and scope: name, origin, verifiable-response spec id, build version, dataset statistics (documents per source, total chunks), and last-updated.

Parameters
{}
Example response
{
  "name": "rise-ai-sweden",
  "server": "rise-ai-sweden.jardenberg.org",
  "origin": "https://rise-ai-sweden.jardenberg.org",
  "endpoint": "https://rise-ai-sweden.jardenberg.org/api/mcp",
  "serverOperator": "Studio Jardenberg",
  "version": "v202608211730",
  "spec": "org.jardenberg/verifiable-mcp",
  "specVersion": "0.2.1",
  "authentication": "none",
  "rateLimit": "60 requests / 5 minutes per IP",
  "stats": {
    "documents": 2266,
    "chunks": 41230,
    "sources": [
      { "source": "rise", "documents": 650, "lastUpdated": "2026-08-21T08:03:34Z" },
      { "source": "ai_sweden", "documents": 1616, "lastUpdated": "2026-08-21T07:58:10Z" }
    ]
  },
  "lastUpdated": "2026-08-21T08:03:34Z"
}

Trust & verification

Every tools/call response is cryptographically signed and carries a provenance block (spec org.jardenberg/verifiable-mcp 0.2.1). This is a working pilot of verifiable MCP responses — in the spirit of C2PA content credentials, but for tool output. No MCP client verifies these signatures automatically yet; the point is that you can, today, with any standard JOSE library.

What is signed

  • Scope: a wrapper object { iat, payload, payload_digest, content_digest, provenance } payload is the tool result data exactly as returned (no provenance mirrored inside it). Both digests are inside the signature.
  • Envelope location: the JSON-RPC result's _meta["org.jardenberg/verifiable-mcp"] = { spec, alg, kid, signed: "wrapper", jws }. Nothing security-bearing lives outside the JWS — a verifier trusts only values recovered from the verified wrapper.
  • Algorithm: EdDSA (Ed25519), compact JWS, protected header typ: "verifiable-mcp+jws" (RFC 8725 explicit typing). Canonicalization: RFC 8785 (JCS) — normative for the JWS payload and every digest.
  • Content binding by digest: content_digest is sha256:<hex> over the exact served bytes of content[0].text (canonical JSON on this server). Hash the rendered text and compare it against the signed digest; payload_digest covers the RFC 8785 canonical payload.
  • Key discovery: /.well-known/mcp.json (server card with a jwks key set), or the dedicated JWK at /.well-known/rise-ai-sweden-mcp-public-key.json (kid = RFC 7638 thumbprint). The server-card path follows SEP-2127, which superseded SEP-1649 and settled /.well-known/mcp.json — both the card and the dedicated key file are served.
  • JSON-RPC error frames carry the envelope at error.data["org.jardenberg/verifiable-mcp"] (strict SDKs drop unknown members of error), with { id, error } — including the request id, envelope removed — as the wrapper payload. Tool-level isError results are signed like any result, wrapper payload { isError: true, message }.
  • Provenance inside the wrapper: server_operator, server, legal_basis, dataset_version and last_updated — plus content_publisher and a string canonical_origin for single-source responses, or a publishers[] array (and no canonical_origin) when a response spans both sources. No content_hash* keys — the two digests above are the binding. Content is published by RISE and AI Sweden and remains © its publisher: indexed under the EU TDM exception (DSM directive, art. 4), excerpts served with source attribution. This server claims no licence over it.
  • structuredContent is byte-identical (RFC 8785) to the signed wrapper.payload — nothing is injected into it. The deprecated v0.1 result.signature and result.provenance mirror are emitted as top-level result siblings (removed in v0.3).

Verify it yourself — three steps

  1. Fetch /.well-known/mcp.json and pick the JWK from jwks.keys whose kid matches the envelope.
  2. Verify the compact JWS (header.payload.signature) with EdDSA — jose (JS) or python-jose / PyJWT (Python). The verified payload is the JCS-canonical wrapper.
  3. Hash the served content[0].text bytes and compare with the signed wrapper.content_digest; check wrapper.payload_digest against the SHA-256 of the RFC 8785 canonical wrapper.payload.
// Node — npm i jose canonicalize
import { compactVerify, importJWK } from "jose";
import canon from "canonicalize";                     // RFC 8785 (JCS)

const card = await (await fetch("https://rise-ai-sweden.jardenberg.org/.well-known/mcp.json")).json();
const jwk = card.jwks.keys[0];
const key = await importJWK({ kty: jwk.kty, crv: jwk.crv, x: jwk.x }, "EdDSA");

const { result } = mcpResponse;                       // one tools/call result
const env = result._meta["org.jardenberg/verifiable-mcp"];
const { payload } = await compactVerify(env.jws, key);
const wrapper = JSON.parse(new TextDecoder().decode(payload));

const sha = async (s) => "sha256:" + [...new Uint8Array(await crypto.subtle.digest(
  "SHA-256", new TextEncoder().encode(s)))]
  .map((b) => b.toString(16).padStart(2, "0")).join("");

// digests come from the VERIFIED wrapper, never from _meta
console.log(await sha(result.content[0].text) === wrapper.content_digest); // content binding
console.log(await sha(canon(wrapper.payload)) === wrapper.payload_digest);

If the signing key is ever unavailable, responses are served unsigned — signing never blocks a query. Data fields are unchanged by this layer; the envelope and provenance are purely additive.

FAQ

Are responses signed?
Yes — every tools/call response carries an Ed25519 (EdDSA) compact JWS over a { iat, payload, provenance } wrapper, RFC 8785 (JCS) canonical, in result._meta["org.jardenberg/verifiable-mcp"]. The deprecated v0.1 result.signature is still emitted. See Trust & verification above. Purely additive — no existing data field changed.
Is this open source?
Yes — code lives at github.com/jardenberg/swedish-ai-unlocked. Issues and PRs welcome.
Do I need an API key?
No. Public endpoint, no auth. Rate limit: 60 requests / 5 minutes per IP.
What's indexed?
Public web pages and PDFs from ri.se and ai.se. No paywalled or internal content. Use list_sources for current counts and the most recent fetch per source.
How fresh is it?
Sitemaps are re-checked automatically every day at 04:15 UTC. New and changed documents are scraped, cleaned, chunked, and embedded in the same pass. Last completed refresh: 2026-09-11 04:15 UTC. list_sources always reports the live per-source last-fetch timestamps.
Why does find_mentions return fewer rows than `total`?
total is a reconciliation count — distinct documents whose markdown contains the exact substring, matching a direct SQL ILIKE query. returned is what the tool hands back, capped by limit (max 100).
Why doesn't my AI Sweden event URL show up?
Many ai.se event pages 301-redirect to third-party sites (Invajo, Meetup, etc.). Those are deliberately skipped — the index only stores content served from ri.se and ai.se themselves.
How short-query search works
For queries of ≤2 tokens, the lexical arm is weighted 2× over the vector arm. Good for proper nouns and organization names; if a one-word topical query feels too literal, add a second word.
Can I trust the dates?
publishedAt comes from page metadata or PDF path when available, falling back to sitemap last-modified.publishedAtSource tells you which (meta · path · sitemap).
Why does a PDF look OCR'd?
Image-only PDFs and oversized files (~>20 MB) are routed through Firecrawl OCR. get_document surfaces this via extractionMethod: "firecrawl" and a human-readable contentNote.
What does page_type mean?
It's URL-derived, not editorial. project means the URL lives under a /projects/ path, not that someone curated it as a project.