@aborruso/ckan-mcp-server 0.4.87 → 0.4.88

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # LOG
2
2
 
3
+ ## 2026-03-19
4
+
5
+ ### v0.4.88
6
+
7
+ - fix(`tools/organization.ts`): `ckan_organization_search` now shows `view_url` in markdown table and JSON output; `ckan_organization_show` JSON includes `view_url` — all using `portals.json` custom patterns
8
+
9
+
10
+
11
+ - evals(`evals/tool-selection`): tool selection eval pipeline — synthetic NL query generation (Gemini, 1583 records), train/eval split, fine-tuning Qwen2.5-0.5B with Unsloth on Colab T4; 86.3% accuracy on 8 tools; model published at huggingface.co/aborruso/ckan-tool-selector
12
+
3
13
  ## 2026-03-17
4
14
 
5
15
  - fix(`tools/sparql.ts`): add `; charset=utf-8` to POST Content-Type — fixes accented chars corruption in SPARQL queries (issue #22)
package/dist/index.js CHANGED
@@ -1919,7 +1919,7 @@ function compactOrganizationList(result) {
1919
1919
  })
1920
1920
  };
1921
1921
  }
1922
- function compactOrganizationShow(result) {
1922
+ function compactOrganizationShow(result, serverUrl) {
1923
1923
  return {
1924
1924
  id: result.id,
1925
1925
  name: result.name,
@@ -1928,6 +1928,7 @@ function compactOrganizationShow(result) {
1928
1928
  image_url: result.image_url || null,
1929
1929
  package_count: result.package_count ?? 0,
1930
1930
  created: result.created || null,
1931
+ view_url: getOrganizationViewUrl(serverUrl, result),
1931
1932
  packages: (result.packages || []).map((pkg) => ({
1932
1933
  id: pkg.id,
1933
1934
  name: pkg.name,
@@ -2174,7 +2175,7 @@ Typical workflow: ckan_organization_show \u2192 ckan_package_show (inspect a dat
2174
2175
  }
2175
2176
  );
2176
2177
  if (params.response_format === "json" /* JSON */) {
2177
- const compact = compactOrganizationShow(result);
2178
+ const compact = compactOrganizationShow(result, params.server_url);
2178
2179
  return {
2179
2180
  content: [{ type: "text", text: truncateJson(compact) }],
2180
2181
  structuredContent: compact
@@ -2251,7 +2252,8 @@ Typical workflow: ckan_organization_search \u2192 ckan_organization_show (get de
2251
2252
  organizations: orgFacets.map((item) => ({
2252
2253
  name: item.name,
2253
2254
  display_name: item.display_name,
2254
- dataset_count: item.count
2255
+ dataset_count: item.count,
2256
+ view_url: getOrganizationViewUrl(params.server_url, { name: item.name })
2255
2257
  }))
2256
2258
  };
2257
2259
  return {
@@ -2281,12 +2283,13 @@ Typical workflow: ckan_organization_search \u2192 ckan_organization_show (get de
2281
2283
  markdown += `## Matching Organizations
2282
2284
 
2283
2285
  `;
2284
- markdown += `| Organization | Datasets |
2286
+ markdown += `| Organization | Datasets | Link |
2285
2287
  `;
2286
- markdown += `|--------------|----------|
2288
+ markdown += `|--------------|----------|------|
2287
2289
  `;
2288
2290
  for (const org of orgFacets) {
2289
- markdown += `| ${org.display_name || org.name} | ${org.count} |
2291
+ const viewUrl = getOrganizationViewUrl(params.server_url, { name: org.name });
2292
+ markdown += `| ${org.display_name || org.name} | ${org.count} | ${viewUrl} |
2290
2293
  `;
2291
2294
  }
2292
2295
  }
@@ -5124,7 +5127,7 @@ var registerAllPrompts = (server2) => {
5124
5127
  function createServer() {
5125
5128
  return new McpServer({
5126
5129
  name: "ckan-mcp-server",
5127
- version: "0.4.87"
5130
+ version: "0.4.88"
5128
5131
  });
5129
5132
  }
5130
5133
  function registerAll(server2) {
package/dist/worker.js CHANGED
@@ -825,7 +825,7 @@ Returns:
825
825
  Total dataset count, categories ranked by count, file formats ranked by count, organizations ranked by count.
826
826
 
827
827
  Typical workflow: ckan_catalog_stats (understand the portal) \u2192 ckan_package_search (query specific data)`,inputSchema:v.object({server_url:v.string().url().describe("Base URL of the CKAN server (e.g., https://dati.comune.messina.it)"),facet_limit:v.number().int().min(1).max(100).optional().default(20).describe("Max entries per facet section (default 20)"),response_format:fe}).strict(),annotations:{readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1}},async e=>{try{let r=await F(e.server_url,"package_search",{q:"*:*",rows:0,"facet.field":JSON.stringify(["groups","res_format","organization"]),"facet.limit":e.facet_limit});if(e.response_format==="json")return{content:[{type:"text",text:W(JSON.stringify({total:r.count,facets:r.facets},null,2))}]};let n=DO(e.server_url,r.count,r.facets);return{content:[{type:"text",text:W(se(n))}]}}catch(r){return{content:[{type:"text",text:`Error retrieving catalog stats: ${r instanceof Error?r.message:String(r)}`}],isError:!0}}})}var Na=25,Yf=1e3;function ZO(t){let e=t.replace(/#[^\n]*/g,"");if(!/\bSELECT\b/i.test(e))throw new Error("Only SELECT queries are supported (not CONSTRUCT, ASK, DESCRIBE, or write operations).")}function LO(t,e){return/\bLIMIT\b/i.test(t)?t:`${t.trimEnd()}
828
- LIMIT ${e}`}async function UO(t,e){if(Hf(t),new URL(t).protocol!=="https:")throw new Error("Only HTTPS endpoints are allowed");let o=Av(t)?.method??"POST",s={Accept:"application/sparql-results+json","User-Agent":"Mozilla/5.0 (compatible; CKAN-MCP-Server/1.0)"},i=new AbortController,a=setTimeout(()=>i.abort(),15e3),c;try{if(o==="GET"){let u=new URL(t);u.searchParams.set("query",e),c=await fetch(u.toString(),{method:"GET",signal:i.signal,headers:s})}else if(c=await fetch(t,{method:"POST",signal:i.signal,headers:{...s,"Content-Type":"application/sparql-query"},body:e}),c.status===403||c.status===405){let u=new URL(t);u.searchParams.set("query",e),c=await fetch(u.toString(),{method:"GET",signal:i.signal,headers:s})}}finally{clearTimeout(a)}if(!c.ok)throw new Error(`SPARQL endpoint error (${c.status}): ${c.statusText}`);return c.json()}function FO(t,e){let r=t.head.vars,n=t.results.bindings,o=`# SPARQL Query Results
828
+ LIMIT ${e}`}async function UO(t,e){if(Hf(t),new URL(t).protocol!=="https:")throw new Error("Only HTTPS endpoints are allowed");let o=Av(t)?.method??"POST",s={Accept:"application/sparql-results+json","User-Agent":"Mozilla/5.0 (compatible; CKAN-MCP-Server/1.0)"},i=new AbortController,a=setTimeout(()=>i.abort(),15e3),c;try{if(o==="GET"){let u=new URL(t);u.searchParams.set("query",e),c=await fetch(u.toString(),{method:"GET",signal:i.signal,headers:s})}else if(c=await fetch(t,{method:"POST",signal:i.signal,headers:{...s,"Content-Type":"application/sparql-query; charset=utf-8"},body:e}),c.status===403||c.status===405){let u=new URL(t);u.searchParams.set("query",e),c=await fetch(u.toString(),{method:"GET",signal:i.signal,headers:s})}}finally{clearTimeout(a)}if(!c.ok)throw new Error(`SPARQL endpoint error (${c.status}): ${c.statusText}`);return c.json()}function FO(t,e){let r=t.head.vars,n=t.results.bindings,o=`# SPARQL Query Results
829
829
 
830
830
  `;if(o+=`**Endpoint**: ${e}
831
831
  `,o+=`**Rows**: ${n.length}
@@ -1086,7 +1086,7 @@ ckan_package_search({
1086
1086
  })
1087
1087
  \`\`\`
1088
1088
 
1089
- If the portal supports HVD classification, look for datasets with fields like \`hvd_category\` or \`applicable_legislation\`.`,$b=t=>{t.registerPrompt(aC,{title:"Search High-Value Datasets (HVD)",description:"Guided prompt to find High-Value Datasets (HVD) on a CKAN portal. Automatically uses the correct filter field from portal configuration.",argsSchema:{server_url:v.string().url().describe("Base URL of the CKAN server"),rows:v.coerce.number().int().positive().default(10).describe("Max results to return")}},async({server_url:e,rows:r})=>{let n=so(e);return ht(cC(e,r,n?.category_field??null))})};var Sb=t=>{vb(t),bb(t),wb(t),xb(t),kb(t),$b(t)};function zb(){return new ma({name:"ckan-mcp-server",version:"0.4.86"})}function Tb(t){Kv(t),Jv(t),Gv(t),Qv(t),Yv(t),eb(t),ib(t),ab(t),cb(t),ub(t),lb(t),_b(t),Sb(t)}var ja=class{constructor(e={}){this._started=!1,this._streamMapping=new Map,this._requestToStreamMapping=new Map,this._requestResponseMap=new Map,this._initialized=!1,this._enableJsonResponse=!1,this._standaloneSseStreamId="_GET_stream",this.sessionIdGenerator=e.sessionIdGenerator,this._enableJsonResponse=e.enableJsonResponse??!1,this._eventStore=e.eventStore,this._onsessioninitialized=e.onsessioninitialized,this._onsessionclosed=e.onsessionclosed,this._allowedHosts=e.allowedHosts,this._allowedOrigins=e.allowedOrigins,this._enableDnsRebindingProtection=e.enableDnsRebindingProtection??!1,this._retryInterval=e.retryInterval}async start(){if(this._started)throw new Error("Transport already started");this._started=!0}createJsonErrorResponse(e,r,n,o){let s={code:r,message:n};return o?.data!==void 0&&(s.data=o.data),new Response(JSON.stringify({jsonrpc:"2.0",error:s,id:null}),{status:e,headers:{"Content-Type":"application/json",...o?.headers}})}validateRequestHeaders(e){if(this._enableDnsRebindingProtection){if(this._allowedHosts&&this._allowedHosts.length>0){let r=e.headers.get("host");if(!r||!this._allowedHosts.includes(r)){let n=`Invalid Host header: ${r}`;return this.onerror?.(new Error(n)),this.createJsonErrorResponse(403,-32e3,n)}}if(this._allowedOrigins&&this._allowedOrigins.length>0){let r=e.headers.get("origin");if(r&&!this._allowedOrigins.includes(r)){let n=`Invalid Origin header: ${r}`;return this.onerror?.(new Error(n)),this.createJsonErrorResponse(403,-32e3,n)}}}}async handleRequest(e,r){let n=this.validateRequestHeaders(e);if(n)return n;switch(e.method){case"POST":return this.handlePostRequest(e,r);case"GET":return this.handleGetRequest(e);case"DELETE":return this.handleDeleteRequest(e);default:return this.handleUnsupportedRequest()}}async writePrimingEvent(e,r,n,o){if(!this._eventStore||o<"2025-11-25")return;let s=await this._eventStore.storeEvent(n,{}),i=`id: ${s}
1089
+ If the portal supports HVD classification, look for datasets with fields like \`hvd_category\` or \`applicable_legislation\`.`,$b=t=>{t.registerPrompt(aC,{title:"Search High-Value Datasets (HVD)",description:"Guided prompt to find High-Value Datasets (HVD) on a CKAN portal. Automatically uses the correct filter field from portal configuration.",argsSchema:{server_url:v.string().url().describe("Base URL of the CKAN server"),rows:v.coerce.number().int().positive().default(10).describe("Max results to return")}},async({server_url:e,rows:r})=>{let n=so(e);return ht(cC(e,r,n?.category_field??null))})};var Sb=t=>{vb(t),bb(t),wb(t),xb(t),kb(t),$b(t)};function zb(){return new ma({name:"ckan-mcp-server",version:"0.4.87"})}function Tb(t){Kv(t),Jv(t),Gv(t),Qv(t),Yv(t),eb(t),ib(t),ab(t),cb(t),ub(t),lb(t),_b(t),Sb(t)}var ja=class{constructor(e={}){this._started=!1,this._streamMapping=new Map,this._requestToStreamMapping=new Map,this._requestResponseMap=new Map,this._initialized=!1,this._enableJsonResponse=!1,this._standaloneSseStreamId="_GET_stream",this.sessionIdGenerator=e.sessionIdGenerator,this._enableJsonResponse=e.enableJsonResponse??!1,this._eventStore=e.eventStore,this._onsessioninitialized=e.onsessioninitialized,this._onsessionclosed=e.onsessionclosed,this._allowedHosts=e.allowedHosts,this._allowedOrigins=e.allowedOrigins,this._enableDnsRebindingProtection=e.enableDnsRebindingProtection??!1,this._retryInterval=e.retryInterval}async start(){if(this._started)throw new Error("Transport already started");this._started=!0}createJsonErrorResponse(e,r,n,o){let s={code:r,message:n};return o?.data!==void 0&&(s.data=o.data),new Response(JSON.stringify({jsonrpc:"2.0",error:s,id:null}),{status:e,headers:{"Content-Type":"application/json",...o?.headers}})}validateRequestHeaders(e){if(this._enableDnsRebindingProtection){if(this._allowedHosts&&this._allowedHosts.length>0){let r=e.headers.get("host");if(!r||!this._allowedHosts.includes(r)){let n=`Invalid Host header: ${r}`;return this.onerror?.(new Error(n)),this.createJsonErrorResponse(403,-32e3,n)}}if(this._allowedOrigins&&this._allowedOrigins.length>0){let r=e.headers.get("origin");if(r&&!this._allowedOrigins.includes(r)){let n=`Invalid Origin header: ${r}`;return this.onerror?.(new Error(n)),this.createJsonErrorResponse(403,-32e3,n)}}}}async handleRequest(e,r){let n=this.validateRequestHeaders(e);if(n)return n;switch(e.method){case"POST":return this.handlePostRequest(e,r);case"GET":return this.handleGetRequest(e);case"DELETE":return this.handleDeleteRequest(e);default:return this.handleUnsupportedRequest()}}async writePrimingEvent(e,r,n,o){if(!this._eventStore||o<"2025-11-25")return;let s=await this._eventStore.storeEvent(n,{}),i=`id: ${s}
1090
1090
  data:
1091
1091
 
1092
1092
  `;this._retryInterval!==void 0&&(i=`id: ${s}
@@ -1280,4 +1280,4 @@ data:
1280
1280
  </div>
1281
1281
  </div>
1282
1282
  </body>
1283
- </html>`,{headers:{"Content-Type":"text/html; charset=utf-8","Access-Control-Allow-Origin":"*"}});if(t.method==="GET"&&e.pathname==="/health")return new Response(JSON.stringify({status:"ok",version:"0.4.86",tools:20,resources:7,prompts:6,runtime:"cloudflare-workers"}),{headers:{"Content-Type":"application/json","Access-Control-Allow-Origin":"*"}});if(e.pathname==="/mcp"){if(t.method==="OPTIONS")return new Response(null,{status:204,headers:{"Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, POST, DELETE, OPTIONS","Access-Control-Allow-Headers":"Content-Type, Accept, Authorization, Mcp-Session-Id","Access-Control-Max-Age":"86400"}});try{let r=t.clone();try{let s=await r.json();if(s?.method==="tools/call"&&s?.params?.name){let i=s.params.name,a=s.params.arguments??{},c={tool:i,server:a.server_url??a.endpoint_url??""};a.q!==void 0&&(c.q=a.q),a.fq!==void 0&&(c.fq=a.fq),a.query!==void 0&&(c.query=a.query),a.id!==void 0&&(c.id=a.id),a.name!==void 0&&(c.name=a.name),a.pattern!==void 0&&(c.pattern=a.pattern),a.resource_id!==void 0&&(c.resource_id=a.resource_id),a.format_filter!==void 0&&(c.format_filter=a.format_filter),a.sort!==void 0&&(c.sort=a.sort),a.rows!==void 0&&(c.rows=a.rows),a.limit!==void 0&&(c.limit=a.limit),a.sql!==void 0&&(c.sql=String(a.sql).slice(0,200)),a.country!==void 0&&(c.country=a.country),a.language!==void 0&&(c.language=a.language),a.has_datastore!==void 0&&(c.has_datastore=a.has_datastore),a.min_datasets!==void 0&&(c.min_datasets=a.min_datasets),console.log(JSON.stringify(c))}}catch{}let n=await Eb.handleRequest(t),o=new Headers(n.headers);return o.set("Access-Control-Allow-Origin","*"),o.set("X-Service-Notice","Demo instance - 100k requests/day shared quota"),o.set("X-Recommendation","https://github.com/ondata/ckan-mcp-server#installation"),new Response(n.body,{status:n.status,statusText:n.statusText,headers:o})}catch(r){return console.error("Worker error:",r),new Response(JSON.stringify({jsonrpc:"2.0",error:{code:-32603,message:"Internal error",data:r instanceof Error?r.message:String(r)},id:null}),{status:500,headers:{"Content-Type":"application/json","Access-Control-Allow-Origin":"*"}})}}return new Response("Not Found",{status:404,headers:{"Access-Control-Allow-Origin":"*"}})}};export{oF as default};
1283
+ </html>`,{headers:{"Content-Type":"text/html; charset=utf-8","Access-Control-Allow-Origin":"*"}});if(t.method==="GET"&&e.pathname==="/health")return new Response(JSON.stringify({status:"ok",version:"0.4.87",tools:20,resources:7,prompts:6,runtime:"cloudflare-workers"}),{headers:{"Content-Type":"application/json","Access-Control-Allow-Origin":"*"}});if(e.pathname==="/mcp"){if(t.method==="OPTIONS")return new Response(null,{status:204,headers:{"Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, POST, DELETE, OPTIONS","Access-Control-Allow-Headers":"Content-Type, Accept, Authorization, Mcp-Session-Id","Access-Control-Max-Age":"86400"}});try{let r=t.clone();try{let s=await r.json();if(s?.method==="tools/call"&&s?.params?.name){let i=s.params.name,a=s.params.arguments??{},c={tool:i,server:a.server_url??a.endpoint_url??""};a.q!==void 0&&(c.q=a.q),a.fq!==void 0&&(c.fq=a.fq),a.query!==void 0&&(c.query=a.query),a.id!==void 0&&(c.id=a.id),a.name!==void 0&&(c.name=a.name),a.pattern!==void 0&&(c.pattern=a.pattern),a.resource_id!==void 0&&(c.resource_id=a.resource_id),a.format_filter!==void 0&&(c.format_filter=a.format_filter),a.sort!==void 0&&(c.sort=a.sort),a.rows!==void 0&&(c.rows=a.rows),a.limit!==void 0&&(c.limit=a.limit),a.sql!==void 0&&(c.sql=String(a.sql).slice(0,200)),a.country!==void 0&&(c.country=a.country),a.language!==void 0&&(c.language=a.language),a.has_datastore!==void 0&&(c.has_datastore=a.has_datastore),a.min_datasets!==void 0&&(c.min_datasets=a.min_datasets),console.log(JSON.stringify(c))}}catch{}let n=await Eb.handleRequest(t),o=new Headers(n.headers);return o.set("Access-Control-Allow-Origin","*"),o.set("X-Service-Notice","Demo instance - 100k requests/day shared quota"),o.set("X-Recommendation","https://github.com/ondata/ckan-mcp-server#installation"),new Response(n.body,{status:n.status,statusText:n.statusText,headers:o})}catch(r){return console.error("Worker error:",r),new Response(JSON.stringify({jsonrpc:"2.0",error:{code:-32603,message:"Internal error",data:r instanceof Error?r.message:String(r)},id:null}),{status:500,headers:{"Content-Type":"application/json","Access-Control-Allow-Origin":"*"}})}}return new Response("Not Found",{status:404,headers:{"Access-Control-Allow-Origin":"*"}})}};export{oF as default};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aborruso/ckan-mcp-server",
3
- "version": "0.4.87",
3
+ "version": "0.4.88",
4
4
  "mcpName": "io.github.aborruso/ckan-mcp-server",
5
5
  "description": "MCP server for interacting with CKAN open data portals",
6
6
  "main": "dist/index.js",