<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[OpenData Forge]]></title><description><![CDATA[OpenData Forge]]></description><link>https://opendata-forge.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6ad36631de096097dbaea983/a66e1894-035d-4d89-8edb-a23f31c6bf46.png</url><title>OpenData Forge</title><link>https://opendata-forge.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 20 Oct 2026 19:04:09 GMT</lastBuildDate><atom:link href="https://opendata-forge.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Give Claude/Cursor access to Spain's official gazette (BOE) via MCP]]></title><description><![CDATA[Disclosure: I maintain the open-source MCP server used here (MIT licence) and its hosted versions on MCPize (free plan plus paid plans, so I earn from paid…]]></description><link>https://opendata-forge.hashnode.dev/give-claude-cursor-access-to-spain-s-official-gazette-boe-via-mcp</link><guid isPermaLink="true">https://opendata-forge.hashnode.dev/give-claude-cursor-access-to-spain-s-official-gazette-boe-via-mcp</guid><category><![CDATA[mcp]]></category><category><![CDATA[AI]]></category><category><![CDATA[#OpenData]]></category><category><![CDATA[Spain]]></category><dc:creator><![CDATA[OpenData Forge]]></dc:creator><pubDate>Sat, 17 Oct 2026 16:59:20 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p><strong>Disclosure:</strong> I maintain the open-source MCP server used here (MIT licence) and its hosted versions on MCPize (free plan plus paid plans, so I earn from paid subscriptions) and Smithery (free). <em>This article was written with AI assistance, and every command was run and checked before publishing.</em></p>
</blockquote>
<p>Spain's <strong>Boletín Oficial del Estado (BOE)</strong> publishes new laws, grants, public jobs and tender notices every day, and its sister bulletin <strong>BORME</strong> publishes company registry acts. Both have an official open-data API, but asking an AI assistant "any SME grants in the BOE this week?" normally gets you a stale or made-up answer.</p>
<p>The <a href="https://modelcontextprotocol.io">Model Context Protocol</a> fixes that by giving the assistant a real tool to call. In about five minutes you'll have Claude Desktop or Cursor querying the live BOE, plus EU funding calls and EU tenders (TED).</p>
<h2>The server</h2>
<p><a href="https://github.com/thehomer87/eu-spain-public-data-mcp"><code>eu-spain-public-data-mcp</code></a> exposes three read-only tools, and none of them need an API key:</p>
<table>
<thead>
<tr>
<th>Tool</th>
<th>Source</th>
</tr>
</thead>
<tbody><tr>
<td><code>search_boe_borme</code></td>
<td>BOE / BORME daily summaries (keywords, sections, date range up to 31 days)</td>
</tr>
<tr>
<td><code>search_eu_funding</code></td>
<td>EU Funding &amp; Tenders Portal: open and forthcoming calls, sorted by deadline</td>
</tr>
<tr>
<td><code>search_ted_tenders</code></td>
<td>TED procurement notices (keyword, buyer country, CPV, date)</td>
</tr>
</tbody></table>
<h2>Option A: run it locally (free)</h2>
<p>Requires Node 20+.</p>
<pre><code class="language-bash">git clone https://github.com/thehomer87/eu-spain-public-data-mcp
cd eu-spain-public-data-mcp
npm ci &amp;&amp; npm run build
</code></pre>
<p><strong>Claude Desktop:</strong> edit <code>claude_desktop_config.json</code> (Settings → Developer → Edit Config):</p>
<pre><code class="language-json">{
  "mcpServers": {
    "spain-eu-data": {
      "command": "node",
      "args": ["/absolute/path/eu-spain-public-data-mcp/dist/index.js", "--stdio"]
    }
  }
}
</code></pre>
<p><strong>Cursor:</strong> put the same block in <code>~/.cursor/mcp.json</code> (global) or <code>.cursor/mcp.json</code> (per project), then enable it under Settings → MCP.</p>
<p>Restart the app. You should see three tools under "spain-eu-data".</p>
<h2>Option B: hosted, nothing to install</h2>
<p>The same server runs on <a href="https://mcpize.com/mcp/eu-spain-public-data">MCPize</a>. Subscribe to the free plan (50 calls a month) and copy the connection snippet MCPize shows you into the same config file. Hosting has paid tiers if you need more calls.</p>
<h2>Option C: Smithery</h2>
<p>The server is also listed on <a href="https://smithery.ai/servers/opendata-forge/eu-spain-public-data">Smithery</a>, which generates the config for Claude Desktop, Cursor and other clients in one click.</p>
<h2>Check it works (without an AI client)</h2>
<p>Before blaming your config, talk to the server directly over stdio. The repo ships a smoke test that starts the server, lists the tools and makes four real calls:</p>
<pre><code class="language-bash">cd eu-spain-public-data-mcp
npm test
</code></pre>
<p>The output from a fresh clone on 17 Oct 2026 (abridged):</p>
<pre><code>stdio tools: search_boe_borme, search_eu_funding, search_ted_tenders
search_boe_borme {"keywords":["subvenci"],"maxResults":5} -&gt; 5 items in 1568ms | first: "Real Decreto 832/2026, de 14 de octubre, por el que se regula la concesión directa de una subvención a la Fun…
search_boe_borme {"bulletins":["BORME"],"maxResults":3} -&gt; 3 items in 257ms
search_eu_funding {"keywords":"artificial intelligence","maxResults":5} -&gt; 5 items in 1038ms | first: "Artificial Intelligence for Cybersecurity applications"
search_ted_tenders {"keywords":"software","countries":["ESP"],"maxResults":5} -&gt; 5 items in 670ms
</code></pre>
<p>If that works but your AI client shows no tools, the problem is the config path or the client wasn't restarted. A good first BOE call to try, because the parameter names matter:</p>
<pre><code class="language-json">{ "keywords": ["ayudas", "subvenciones"], "sections": ["3", "5B"], "dateFrom": "2026-10-12", "dateTo": "2026-10-17", "maxResults": 3 }
</code></pre>
<h2>Prompts that work well</h2>
<ul>
<li>"Search the BOE from the last 7 days for grants (sections 3 and 5B) mentioning <em>digitalización</em> and summarise who can apply."</li>
<li>"List open EU calls about artificial intelligence closing in the next 90 days, as a table with deadline and programme."</li>
<li>"Find Spanish software tenders (CPV 72000000) published on TED since Monday."</li>
<li>"What did BORME publish yesterday about company formations in Madrid?" (use <code>bulletins: ["BORME"]</code>)</li>
</ul>
<h2>BOE section codes cheat sheet</h2>
<p><code>1</code> general provisions (laws, royal decrees) · <code>2A</code>/<code>2B</code> appointments and public exams · <code>3</code> other provisions (many grant rules) · <code>5A</code> tender notices · <code>5B</code> other notices (grant extracts) · BORME <code>A</code>/<code>B</code> company acts.</p>
<h2>Gotchas I hit while testing</h2>
<ul>
<li>Keyword matching covers the title <strong>and</strong> the department name, so "digital" matches everything published by the <em>Ministerio para la Transformación Digital</em>. Combine keywords with <code>sections</code>.</li>
<li>Unknown argument names are silently ignored. <code>from</code> and <code>limit</code> don't work: use <code>dateFrom</code> and <code>maxResults</code>.</li>
<li>A range is capped at 31 days per call. Ask the assistant to page through months.</li>
</ul>
<p><em>Data: Agencia Estatal Boletín Oficial del Estado (<a href="http://www.boe.es">www.boe.es</a>); © European Union (EU Funding &amp; Tenders Portal, TED), reused under Commission Decision 2011/833/EU. Not affiliated with or endorsed by these bodies.</em></p>
]]></content:encoded></item><item><title><![CDATA[Monitor EU public tenders (TED) for your keywords with Apify and n8n]]></title><description><![CDATA[Disclosure: I built and maintain the TED Actor used here. It's a paid Actor on the Apify Store ($1.50 per 1,000 results), so I earn a share when you run…]]></description><link>https://opendata-forge.hashnode.dev/monitor-eu-public-tenders-ted-for-your-keywords-with-apify-and-n8n</link><guid isPermaLink="true">https://opendata-forge.hashnode.dev/monitor-eu-public-tenders-ted-for-your-keywords-with-apify-and-n8n</guid><category><![CDATA[automation]]></category><category><![CDATA[n8n]]></category><category><![CDATA[#OpenData]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[OpenData Forge]]></dc:creator><pubDate>Sat, 17 Oct 2026 16:58:46 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p><strong>Disclosure:</strong> I built and maintain the TED Actor used here. It's a paid Actor on the Apify Store ($1.50 per 1,000 results), so I earn a share when you run it. <em>This article was written with AI assistance, and every command and workflow was run and checked before publishing.</em></p>
</blockquote>
<p>Every year the EU's <strong>Tenders Electronic Daily (TED)</strong> publishes hundreds of thousands of procurement notices. If you sell software, consulting or hardware to the public sector, a 9 a.m. message with <em>only the new, still-open tenders matching your keywords</em> saves hours of portal searching.</p>
<p>In this tutorial we build that alert:</p>
<ol>
<li>An Apify Actor queries the official TED Search API v3 (no scraping, no TED API key).</li>
<li>An n8n workflow runs it every weekday, drops notices you've already seen and anything already closed, and sends you a digest (email, Slack, Telegram, whatever you prefer).</li>
</ol>
<p>Total cost: the Actor charges $1.50 per 1,000 results, so 50 results every weekday is roughly $1.70 a month, which fits inside the $5 monthly credit on Apify's free plan. Add n8n (free if self-hosted, or an n8n Cloud plan).</p>
<h2>1. Try the Actor</h2>
<p>Open the <a href="https://apify.com/opendata-forge/eu-public-tenders-ted-search">EU Public Tenders (TED) Actor</a> on Apify (<a href="https://apify.com/sign-up">create a free account</a> if you don't have one) and run it with this input:</p>
<pre><code class="language-json">{
  "keywords": "ciberseguridad",
  "countries": ["ESP", "PRT"],
  "publishedAfter": "2026-10-01",
  "maxResults": 20
}
</code></pre>
<p>Countries are ISO-3 codes. You can also pass <code>cpvCodes</code>, e.g. <code>["72000000"]</code> for IT services. When I ran this on 17 Oct 2026, it returned 19 notices in about 2 seconds. Each item looks like this:</p>
<pre><code class="language-json">{
  "publicationNumber": "707641-2026",
  "title": "Spain – IT services: consulting, software development, Internet and support – Servicio Integral de Ciberseguridad SIC27",
  "buyer": "Compras de la Corporación de Radio y Televisión Española S.A.",
  "country": "ESP",
  "publicationDate": "2026-10-14+02:00",
  "deadline": "2026-11-19+01:00",
  "cpv": ["72000000"],
  "noticeType": "cn-standard",
  "url": "https://ted.europa.eu/en/notice/-/detail/707641-2026"
}
</code></pre>
<p>Two things I learned from the real output:</p>
<ul>
<li><strong><code>noticeType</code> matters.</strong> Keyword searches also return contract <em>award</em> notices (<code>can-*</code>), which are useless for bidding. Keep <code>cn-*</code> (contract notices).</li>
<li><strong>Full-text search is broad.</strong> "ciberseguridad" also matched an EU furniture framework whose text mentions cybersecurity, and <code>buyer-country</code> matches multi-country EU notices. Add CPV codes if you want precision.</li>
</ul>
<h2>2. Get your Apify API token</h2>
<p>In Apify Console go to <strong>Settings → API &amp; Integrations</strong> and copy your personal API token. In n8n, store it as an environment variable <code>APIFY_TOKEN</code> (self-hosted). On n8n Cloud, or if your instance blocks <code>$env</code> access, create a <strong>Query Auth</strong> credential instead (name <code>token</code>, value = your token) and select it in the HTTP node. Don't hard-code it in the workflow.</p>
<h2>3. Import the n8n workflow</h2>
<p>Copy the workflow JSON from the appendix at the end of this post and paste it onto an empty n8n canvas (Ctrl/Cmd+V), or save it as a <code>.json</code> file and use <strong>Import from File</strong>. It has four nodes:</p>
<table>
<thead>
<tr>
<th>Node</th>
<th>What it does</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Every weekday 08:00</strong></td>
<td>Schedule trigger, cron <code>0 8 * * 1-5</code></td>
</tr>
<tr>
<td><strong>Run TED Actor</strong></td>
<td><code>POST https://api.apify.com/v2/acts/opendata-forge~eu-public-tenders-ted-search/run-sync-get-dataset-items?token=…</code>, which runs the Actor and returns its dataset in one call</td>
</tr>
<tr>
<td><strong>Only new open tenders</strong></td>
<td>Code node: keeps <code>cn-*</code> notices whose deadline hasn't passed and that haven't been alerted before (stored in workflow static data)</td>
</tr>
<tr>
<td><strong>Build digest</strong></td>
<td>Code node: turns the items into a subject and text body</td>
</tr>
</tbody></table>
<p>The Actor ID (<code>opendata-forge~eu-public-tenders-ted-search</code>) is already filled in. The request body uses n8n expressions so it always looks back two days:</p>
<pre><code class="language-json">{
  "keywords": "ciberseguridad",
  "countries": ["ESP", "PRT"],
  "publishedAfter": "{{ $now.minus({days: 2}).toFormat('yyyy-MM-dd') }}",
  "maxResults": 50
}
</code></pre>
<p>The dedupe logic in <strong>Only new open tenders</strong>:</p>
<pre><code class="language-js">const seen = $getWorkflowStaticData('global');
seen.ids = seen.ids || [];
const today = new Date().toISOString().slice(0, 10);
const fresh = [];
for (const item of $input.all()) {
  const n = item.json;
  if (!n.noticeType || !n.noticeType.startsWith('cn')) continue;  // contract notices only
  if (n.deadline &amp;&amp; n.deadline.slice(0, 10) &lt; today) continue;     // already closed
  if (seen.ids.includes(n.publicationNumber)) continue;            // already alerted
  seen.ids.push(n.publicationNumber);
  fresh.push({ json: n });
}
seen.ids = seen.ids.slice(-2000);
return fresh;
</code></pre>
<blockquote>
<p>Static data only persists for <strong>active</strong> (scheduled) workflows, not manual test runs, so expect repeats while you're testing.</p>
</blockquote>
<h2>4. Add your delivery node</h2>
<p>After <strong>Build digest</strong>, add a <strong>Send Email</strong>, <strong>Slack</strong> or <strong>Telegram</strong> node and map <code>{{ $json.subject }}</code> and <code>{{ $json.text }}</code>. To skip empty days, add an <strong>IF</strong> node on <code>{{ $json.count }} &gt; 0</code>.</p>
<p>The digest from my test run (5 of 19 notices survived the filter):</p>
<pre><code>• Spain – IT services: consulting, software development, Internet and support – Servicio Integral de Ciberseguridad SIC27
  Compras de la Corporación de Radio y Televisión Española S.A. | deadline: 2026-11-19 | value: see notice
  https://ted.europa.eu/en/notice/-/detail/707641-2026
</code></pre>
<h2>5. How I tested this</h2>
<ul>
<li>Ran the Actor code locally against the live TED API with the input above (19 notices).</li>
<li>Ran the n8n workflow (n8n 1.123) from the CLI, with the HTTP node pointed at a local mock that returned exactly that dataset. The workflow succeeded and the filter kept 5 open contract notices. The workflow JSON below imports cleanly. I did not call Apify's hosted endpoint in this test, because that needs your token.</li>
</ul>
<h2>Variations</h2>
<ul>
<li>One workflow per client or sector, each with different keywords and CPV codes.</li>
<li>Write rows to Google Sheets or Airtable for a tender pipeline.</li>
<li>Prefer Spanish national tenders? The same pattern works with the <a href="https://apify.com/opendata-forge/boe-borme-daily-search">BOE/BORME daily search Actor</a> and <code>sections: ["5A"]</code>.</li>
</ul>
<p><em>Data: TED – Tenders Electronic Daily, © European Union, reused under Commission Decision 2011/833/EU. This tutorial is not affiliated with the Publications Office of the EU.</em></p>
<h2>Appendix: workflow JSON</h2>
<pre><code class="language-json">{
 "name": "TED tender alerts (Apify + n8n)",
 "nodes": [
  {
   "parameters": {
    "rule": {
     "interval": [
      {
       "field": "cronExpression",
       "expression": "0 8 * * 1-5"
      }
     ]
    }
   },
   "name": "Every weekday 08:00",
   "type": "n8n-nodes-base.scheduleTrigger",
   "typeVersion": 1.2,
   "position": [
    0,
    0
   ]
  },
  {
   "parameters": {
    "method": "POST",
    "url": "https://api.apify.com/v2/acts/opendata-forge~eu-public-tenders-ted-search/run-sync-get-dataset-items",
    "sendQuery": true,
    "queryParameters": {
     "parameters": [
      {
       "name": "token",
       "value": "={{ $env.APIFY_TOKEN }}"
      }
     ]
    },
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "={\n  \"keywords\": \"ciberseguridad\",\n  \"countries\": [\"ESP\", \"PRT\"],\n  \"publishedAfter\": \"{{ $now.minus({days: 2}).toFormat('yyyy-MM-dd') }}\",\n  \"maxResults\": 50\n}",
    "options": {
     "timeout": 300000
    }
   },
   "name": "Run TED Actor",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    260,
    0
   ]
  },
  {
   "parameters": {
    "jsCode": "// Keep only open calls for tender, drop ones already seen in previous runs\nconst seen = $getWorkflowStaticData('global');\nseen.ids = seen.ids || [];\nconst today = new Date().toISOString().slice(0, 10);\nconst fresh = [];\nfor (const item of $input.all()) {\n  const n = item.json;\n  if (!n.noticeType || !n.noticeType.startsWith('cn')) continue;      // contract notices only (no awards)\n  if (n.deadline &amp;&amp; n.deadline.slice(0, 10) &lt; today) continue;         // already closed\n  if (seen.ids.includes(n.publicationNumber)) continue;                // already alerted\n  seen.ids.push(n.publicationNumber);\n  fresh.push({ json: n });\n}\nseen.ids = seen.ids.slice(-2000);\nreturn fresh;"
   },
   "name": "Only new open tenders",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    520,
    0
   ]
  },
  {
   "parameters": {
    "jsCode": "const rows = $input.all().map(i =&gt; i.json);\nif (!rows.length) return [{ json: { count: 0, text: 'No new tenders today.' } }];\nconst lines = rows.map(n =&gt; `• ${n.title}\\n  ${n.buyer ?? ''} | deadline: ${n.deadline ? n.deadline.slice(0,10) : 'n/a'} | ${Number.isFinite(Number(n.estimatedValue)) &amp;&amp; n.estimatedValue ? Math.round(Number(n.estimatedValue)).toLocaleString('en') + ' EUR' : 'value: see notice'}\\n  ${n.url}`);\nreturn [{ json: { count: rows.length, subject: `${rows.length} new EU tenders for your keywords`, text: lines.join('\\n\\n') } }];"
   },
   "name": "Build digest",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    780,
    0
   ]
  }
 ],
 "connections": {
  "Every weekday 08:00": {
   "main": [
    [
     {
      "node": "Run TED Actor",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Run TED Actor": {
   "main": [
    [
     {
      "node": "Only new open tenders",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Only new open tenders": {
   "main": [
    [
     {
      "node": "Build digest",
      "type": "main",
      "index": 0
     }
    ]
   ]
  }
 },
 "settings": {},
 "active": false
}
</code></pre>
]]></content:encoded></item></channel></rss>