@aborruso/ckan-mcp-server 0.4.39 → 0.4.40

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,31 @@
1
1
  # LOG
2
2
 
3
+ ## 2026-02-20
4
+
5
+ - Add DataStore Table Viewer (MCP Apps interactive UI)
6
+ - New MCP resource `ckan-ui://datastore-table` serves self-contained HTML table viewer
7
+ - `ckan_datastore_search` now returns `_meta.ui.resourceUri` + `_meta.ui.data` for MCP Apps clients
8
+ - Interactive features: sortable columns (numeric/date/string type-aware), text filter, pagination (10/25/50/100 rows/page)
9
+ - Works in Node.js and Cloudflare Workers (HTML inlined in TypeScript module, no fs dependency)
10
+ - Non-breaking: text/markdown output unchanged; non-MCP-Apps clients ignore `_meta`
11
+ - Files: `src/ui/datastore-table.html`, `src/resources/datastore-table-ui.ts`, updated `src/tools/datastore.ts` and `src/resources/index.ts`
12
+ - Tests: 228 passing (7 new)
13
+ - Ideas: `docs/future-ideas.md` updated with MCP Apps section
14
+ - OpenSpec: `add-datastore-table-viewer` created and implemented
15
+
16
+ ## 2026-02-09
17
+
18
+ ### Feature Request - One-click Installation
19
+
20
+ - Created issue #11 for one-click MCP server installation support
21
+ - Proposes `claude://install-mcp-server` protocol integration
22
+ - Based on Anthropic Desktop Extensions announcement
23
+ - **Updated proposal**: Two one-click installers to serve different use cases
24
+ - 🚀 **"Try it now"** → HTTP Worker (instant, zero install, shared quota)
25
+ - 💪 **"Install locally"** → npx (unlimited, requires Node.js)
26
+ - User journey: try demo first, install locally when ready for production
27
+ - Files: GitHub issue #11 + comment
28
+
3
29
  ## 2026-02-02
4
30
 
5
31
  ### Release v0.4.39 - Local Install Promotion
@@ -17,7 +43,7 @@
17
43
 
18
44
  **Footer shown to Workers users**:
19
45
  ```
20
- ℹ️ Demo instance (100k requests/month shared globally). For unlimited access: https://github.com/ondata/ckan-mcp-server#installation
46
+ ℹ️ Demo instance (100k requests/day shared quota). For unlimited access: https://github.com/ondata/ckan-mcp-server#installation
21
47
  ```
22
48
 
23
49
  **Files modified**:
@@ -478,10 +504,6 @@
478
504
  - **Fix**: Normalize natural-language queries before search
479
505
  - **Gemini**: Added API key input and NL→Solr query call
480
506
 
481
- ### Web GUI landing + Pages deploy
482
- - **Web GUI**: Added static landing page in `web-gui/public`
483
- - **CI**: Added GitHub Pages workflow for auto deploy on HTML changes
484
-
485
507
  ## 2026-01-10
486
508
 
487
509
  ### Version 0.4.7 - Portal search parser override
package/README.md CHANGED
@@ -24,19 +24,12 @@ MCP (Model Context Protocol) server for interacting with CKAN-based open data po
24
24
 
25
25
  ---
26
26
 
27
- > ## 🚀 **Recommended: Install Locally**
28
- >
27
+ > **💡 Local installation available** for unlimited access and better performance:
29
28
  > ```bash
30
29
  > npm install -g @aborruso/ckan-mcp-server
31
30
  > ```
32
31
  >
33
- > **Benefits:**
34
- > - ✅ **No request limits** - unlimited queries
35
- > - ✅ **Faster** - no network latency
36
- > - ✅ **Always available** - no shared quota
37
- > - ✅ **Free** - open source, no costs
38
- >
39
- > The Cloudflare Workers endpoint is only for quick testing (100k requests/month shared globally across all users).
32
+ > The Cloudflare Workers endpoint has 100k requests/day shared quota - sufficient for most users, but local installation is recommended for heavy usage.
40
33
 
41
34
  ---
42
35
 
package/dist/index.js CHANGED
@@ -57,8 +57,8 @@ var portals_default = {
57
57
  "http://www.data.gov.uk"
58
58
  ],
59
59
  api_path: "/api/action",
60
- dataset_view_url: "https://data.gov.uk/dataset/{name}",
61
- organization_view_url: "https://data.gov.uk/publisher/{name}"
60
+ dataset_view_url: "https://www.data.gov.uk/dataset/{id}/{name}",
61
+ organization_view_url: "https://www.data.gov.uk/organization/{name}"
62
62
  },
63
63
  {
64
64
  id: "catalog-data-gov",
@@ -86,7 +86,9 @@ var portals_default = {
86
86
  "http://data.gov.au",
87
87
  "https://www.data.gov.au",
88
88
  "http://www.data.gov.au"
89
- ]
89
+ ],
90
+ dataset_view_url: "https://data.gov.au/data/dataset/{name}",
91
+ organization_view_url: "https://data.gov.au/data/organization/{name}"
90
92
  },
91
93
  {
92
94
  id: "opendata-swiss",
@@ -98,6 +100,20 @@ var portals_default = {
98
100
  "https://www.opendata.swiss",
99
101
  "http://www.opendata.swiss"
100
102
  ]
103
+ },
104
+ {
105
+ id: "govdata-de",
106
+ name: "govdata.de",
107
+ api_url: "https://ckan.govdata.de",
108
+ api_url_aliases: [
109
+ "https://www.govdata.de",
110
+ "https://govdata.de",
111
+ "https://www.govdata.de/daten",
112
+ "https://data.gov.de",
113
+ "https://www.data.gov.de"
114
+ ],
115
+ dataset_view_url: "https://ckan.govdata.de/dataset/{name}",
116
+ organization_view_url: "https://ckan.govdata.de/organization/{name}"
101
117
  }
102
118
  ],
103
119
  defaults: {
@@ -412,6 +428,20 @@ function formatDate(dateStr) {
412
428
  return "Invalid Date";
413
429
  }
414
430
  }
431
+ function isWorkers() {
432
+ try {
433
+ return typeof WorkerGlobalScope !== "undefined";
434
+ } catch {
435
+ return false;
436
+ }
437
+ }
438
+ function addDemoFooter(text) {
439
+ if (!isWorkers()) {
440
+ return text;
441
+ }
442
+ const footer = "\n\n---\n\u2139\uFE0F Demo instance (100k requests/day shared quota). For unlimited access: https://github.com/ondata/ckan-mcp-server#installation";
443
+ return text + footer;
444
+ }
415
445
 
416
446
  // src/utils/url-generator.ts
417
447
  function getDatasetViewUrl(serverUrl, pkg) {
@@ -1008,7 +1038,7 @@ Note: showing top ${sorted.length} only. Use \`response_format: json\` for full
1008
1038
  `;
1009
1039
  }
1010
1040
  return {
1011
- content: [{ type: "text", text: truncateText(markdown) }]
1041
+ content: [{ type: "text", text: truncateText(addDemoFooter(markdown)) }]
1012
1042
  };
1013
1043
  } catch (error) {
1014
1044
  return {
@@ -1186,7 +1216,7 @@ Examples:
1186
1216
  });
1187
1217
  }
1188
1218
  return {
1189
- content: [{ type: "text", text: truncateText(markdown) }]
1219
+ content: [{ type: "text", text: truncateText(addDemoFooter(markdown)) }]
1190
1220
  };
1191
1221
  } catch (error) {
1192
1222
  return {
@@ -1257,7 +1287,7 @@ Examples:
1257
1287
  }
1258
1288
  const markdown = formatPackageShowMarkdown(result, params.server_url);
1259
1289
  return {
1260
- content: [{ type: "text", text: truncateText(markdown) }]
1290
+ content: [{ type: "text", text: truncateText(addDemoFooter(markdown)) }]
1261
1291
  };
1262
1292
  } catch (error) {
1263
1293
  return {
@@ -1409,7 +1439,7 @@ Note: organization_list returned 500; using package_search facets.
1409
1439
  `;
1410
1440
  }
1411
1441
  return {
1412
- content: [{ type: "text", text: truncateText(markdown2) }]
1442
+ content: [{ type: "text", text: truncateText(addDemoFooter(markdown2)) }]
1413
1443
  };
1414
1444
  }
1415
1445
  throw error;
@@ -1454,7 +1484,7 @@ Note: organization_list returned 500; using package_search facets.
1454
1484
  }
1455
1485
  }
1456
1486
  return {
1457
- content: [{ type: "text", text: truncateText(markdown) }]
1487
+ content: [{ type: "text", text: truncateText(addDemoFooter(markdown)) }]
1458
1488
  };
1459
1489
  } catch (error) {
1460
1490
  return {
@@ -1568,7 +1598,7 @@ ${result.description}
1568
1598
  markdown += "\n";
1569
1599
  }
1570
1600
  return {
1571
- content: [{ type: "text", text: truncateText(markdown) }]
1601
+ content: [{ type: "text", text: truncateText(addDemoFooter(markdown)) }]
1572
1602
  };
1573
1603
  } catch (error) {
1574
1604
  return {
@@ -1672,7 +1702,7 @@ Examples:
1672
1702
  }
1673
1703
  }
1674
1704
  return {
1675
- content: [{ type: "text", text: truncateText(markdown) }]
1705
+ content: [{ type: "text", text: truncateText(addDemoFooter(markdown)) }]
1676
1706
  };
1677
1707
  } catch (error) {
1678
1708
  return {
@@ -1810,7 +1840,19 @@ Examples:
1810
1840
  `;
1811
1841
  }
1812
1842
  return {
1813
- content: [{ type: "text", text: truncateText(markdown) }]
1843
+ content: [{ type: "text", text: truncateText(addDemoFooter(markdown)) }],
1844
+ _meta: {
1845
+ ui: {
1846
+ resourceUri: "ckan-ui://datastore-table",
1847
+ data: {
1848
+ server_url: params.server_url,
1849
+ resource_id: params.resource_id,
1850
+ total: result.total || 0,
1851
+ fields: result.fields || [],
1852
+ records: result.records || []
1853
+ }
1854
+ }
1855
+ }
1814
1856
  };
1815
1857
  } catch (error) {
1816
1858
  return {
@@ -1914,7 +1956,7 @@ Examples:
1914
1956
  markdown += "No records returned by the SQL query.\n";
1915
1957
  }
1916
1958
  return {
1917
- content: [{ type: "text", text: truncateText(markdown) }]
1959
+ content: [{ type: "text", text: truncateText(addDemoFooter(markdown)) }]
1918
1960
  };
1919
1961
  } catch (error) {
1920
1962
  return {
@@ -1971,7 +2013,7 @@ Returns:
1971
2013
  **Site URL**: ${result.site_url || "N/A"}
1972
2014
  `;
1973
2015
  return {
1974
- content: [{ type: "text", text: markdown }],
2016
+ content: [{ type: "text", text: addDemoFooter(markdown) }],
1975
2017
  structuredContent: result
1976
2018
  };
1977
2019
  } catch (error) {
@@ -2106,7 +2148,7 @@ Returns:
2106
2148
  }
2107
2149
  }
2108
2150
  return {
2109
- content: [{ type: "text", text: truncateText(markdown) }]
2151
+ content: [{ type: "text", text: truncateText(addDemoFooter(markdown)) }]
2110
2152
  };
2111
2153
  } catch (error) {
2112
2154
  return {
@@ -2267,7 +2309,7 @@ Returns:
2267
2309
  }
2268
2310
  }
2269
2311
  return {
2270
- content: [{ type: "text", text: truncateText(markdown) }]
2312
+ content: [{ type: "text", text: truncateText(addDemoFooter(markdown)) }]
2271
2313
  };
2272
2314
  } catch (error) {
2273
2315
  return {
@@ -2368,7 +2410,7 @@ ${result.description}
2368
2410
  markdown += "\n";
2369
2411
  }
2370
2412
  return {
2371
- content: [{ type: "text", text: truncateText(markdown) }]
2413
+ content: [{ type: "text", text: truncateText(addDemoFooter(markdown)) }]
2372
2414
  };
2373
2415
  } catch (error) {
2374
2416
  return {
@@ -2468,7 +2510,7 @@ Returns:
2468
2510
  }
2469
2511
  }
2470
2512
  return {
2471
- content: [{ type: "text", text: truncateText(markdown) }]
2513
+ content: [{ type: "text", text: truncateText(addDemoFooter(markdown)) }]
2472
2514
  };
2473
2515
  } catch (error) {
2474
2516
  return {
@@ -3130,7 +3172,7 @@ The MQA (Metadata Quality Assurance) system is operated by data.europa.eu and on
3130
3172
  return {
3131
3173
  content: [{
3132
3174
  type: "text",
3133
- text: output
3175
+ text: addDemoFooter(output)
3134
3176
  }]
3135
3177
  };
3136
3178
  } catch (error) {
@@ -3170,7 +3212,7 @@ The MQA (Metadata Quality Assurance) system is operated by data.europa.eu and on
3170
3212
  return {
3171
3213
  content: [{
3172
3214
  type: "text",
3173
- text: output
3215
+ text: addDemoFooter(output)
3174
3216
  }]
3175
3217
  };
3176
3218
  } catch (error) {
@@ -3443,6 +3485,329 @@ function registerFormatDatasetsResource(server2) {
3443
3485
  });
3444
3486
  }
3445
3487
 
3488
+ // src/resources/datastore-table-ui.ts
3489
+ import { ResourceTemplate as ResourceTemplate5 } from "@modelcontextprotocol/sdk/server/mcp.js";
3490
+ var DATASTORE_TABLE_HTML = `<!DOCTYPE html>
3491
+ <html lang="en">
3492
+ <head>
3493
+ <meta charset="UTF-8">
3494
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
3495
+ <title>DataStore Table Viewer</title>
3496
+ <style>
3497
+ * { box-sizing: border-box; margin: 0; padding: 0; }
3498
+ body {
3499
+ font-family: system-ui, -apple-system, sans-serif;
3500
+ font-size: 13px;
3501
+ background: #fff;
3502
+ color: #1a1a1a;
3503
+ padding: 12px;
3504
+ }
3505
+ .header {
3506
+ display: flex;
3507
+ align-items: center;
3508
+ gap: 8px;
3509
+ margin-bottom: 10px;
3510
+ flex-wrap: wrap;
3511
+ }
3512
+ .title { font-size: 14px; font-weight: 600; color: #333; }
3513
+ .badge {
3514
+ font-size: 11px;
3515
+ color: #555;
3516
+ background: #f0f0f0;
3517
+ border-radius: 10px;
3518
+ padding: 2px 8px;
3519
+ }
3520
+ .controls {
3521
+ display: flex;
3522
+ gap: 8px;
3523
+ margin-bottom: 10px;
3524
+ align-items: center;
3525
+ flex-wrap: wrap;
3526
+ }
3527
+ .filter-input {
3528
+ flex: 1;
3529
+ min-width: 140px;
3530
+ max-width: 300px;
3531
+ padding: 5px 10px;
3532
+ border: 1px solid #d0d0d0;
3533
+ border-radius: 4px;
3534
+ font-size: 12px;
3535
+ outline: none;
3536
+ }
3537
+ .filter-input:focus { border-color: #0066cc; }
3538
+ .page-size-select {
3539
+ padding: 5px 8px;
3540
+ border: 1px solid #d0d0d0;
3541
+ border-radius: 4px;
3542
+ font-size: 12px;
3543
+ background: #fff;
3544
+ }
3545
+ .count-info { font-size: 11px; color: #666; margin-left: auto; }
3546
+ .table-wrapper {
3547
+ overflow-x: auto;
3548
+ border: 1px solid #e0e0e0;
3549
+ border-radius: 4px;
3550
+ }
3551
+ table { width: 100%; border-collapse: collapse; font-size: 12px; }
3552
+ th {
3553
+ background: #f5f5f5;
3554
+ padding: 7px 10px;
3555
+ text-align: left;
3556
+ font-weight: 600;
3557
+ white-space: nowrap;
3558
+ cursor: pointer;
3559
+ user-select: none;
3560
+ border-bottom: 2px solid #e0e0e0;
3561
+ }
3562
+ th:hover { background: #ebebeb; }
3563
+ th.sort-asc::after { content: ' \\25B2'; font-size: 9px; color: #0066cc; }
3564
+ th.sort-desc::after { content: ' \\25BC'; font-size: 9px; color: #0066cc; }
3565
+ td {
3566
+ padding: 5px 10px;
3567
+ border-bottom: 1px solid #f0f0f0;
3568
+ vertical-align: top;
3569
+ max-width: 220px;
3570
+ overflow: hidden;
3571
+ text-overflow: ellipsis;
3572
+ white-space: nowrap;
3573
+ }
3574
+ tr:hover td { background: #fafafa; }
3575
+ tr:last-child td { border-bottom: none; }
3576
+ td.null-val { color: #bbb; font-style: italic; }
3577
+ .pagination {
3578
+ display: flex;
3579
+ align-items: center;
3580
+ gap: 3px;
3581
+ margin-top: 10px;
3582
+ flex-wrap: wrap;
3583
+ }
3584
+ .page-btn {
3585
+ padding: 4px 9px;
3586
+ border: 1px solid #d0d0d0;
3587
+ border-radius: 3px;
3588
+ background: #fff;
3589
+ cursor: pointer;
3590
+ font-size: 12px;
3591
+ line-height: 1.4;
3592
+ }
3593
+ .page-btn:hover:not(:disabled) { background: #f0f0f0; }
3594
+ .page-btn.active { background: #0066cc; color: #fff; border-color: #0066cc; }
3595
+ .page-btn:disabled { opacity: 0.4; cursor: default; }
3596
+ .ellipsis { padding: 4px 4px; font-size: 12px; color: #999; }
3597
+ .notice {
3598
+ font-size: 11px;
3599
+ color: #888;
3600
+ margin-top: 8px;
3601
+ padding: 5px 10px;
3602
+ background: #fafafa;
3603
+ border-left: 3px solid #ddd;
3604
+ border-radius: 0 3px 3px 0;
3605
+ }
3606
+ .state-msg { text-align: center; color: #aaa; padding: 40px 20px; font-size: 13px; }
3607
+ </style>
3608
+ </head>
3609
+ <body>
3610
+ <div id="app"><div class="state-msg">Waiting for data from MCP client&hellip;</div></div>
3611
+ <script>
3612
+ (function () {
3613
+ var allRecords = [], fields = [], total = 0;
3614
+ var filteredRecords = [], currentPage = 1, pageSize = 25;
3615
+ var sortCol = null, sortDir = 1;
3616
+ var filterText = '';
3617
+ var colTypes = {};
3618
+ function escHtml(s) {
3619
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
3620
+ }
3621
+ function range(a, b) { var r=[]; for(var i=a;i<=b;i++) r.push(i); return r; }
3622
+ function pagesToShow(cur, tot) {
3623
+ if (tot<=7) return range(1,tot);
3624
+ if (cur<=4) return range(1,5).concat([tot]);
3625
+ if (cur>=tot-3) return [1].concat(range(tot-4,tot));
3626
+ return [1,cur-1,cur,cur+1,tot];
3627
+ }
3628
+ var dateRe = /^\\d{4}-\\d{2}-\\d{2}/;
3629
+ function inferType(samples) {
3630
+ var numOk=0,dateOk=0,valid=0;
3631
+ for(var i=0;i<samples.length;i++){
3632
+ var v=samples[i];
3633
+ if(v===null||v===undefined||v==='') continue;
3634
+ valid++;
3635
+ var s=String(v);
3636
+ if(s!==''&&!isNaN(Number(s))) numOk++;
3637
+ else if(dateRe.test(s)) dateOk++;
3638
+ }
3639
+ if(valid===0) return 'string';
3640
+ if(numOk/valid>0.8) return 'number';
3641
+ if(dateOk/valid>0.8) return 'date';
3642
+ return 'string';
3643
+ }
3644
+ function detectColTypes(records,fieldList) {
3645
+ var types={};
3646
+ for(var i=0;i<fieldList.length;i++){
3647
+ var fid=fieldList[i].id;
3648
+ var samples=records.slice(0,20).map(function(r){return r[fid];});
3649
+ types[fid]=inferType(samples);
3650
+ }
3651
+ return types;
3652
+ }
3653
+ function cmpValue(a,b,type){
3654
+ if(a===null||a===undefined) return 1;
3655
+ if(b===null||b===undefined) return -1;
3656
+ if(type==='number') return Number(a)-Number(b);
3657
+ if(type==='date') return new Date(a)-new Date(b);
3658
+ return String(a).localeCompare(String(b));
3659
+ }
3660
+ function sortRecords(records){
3661
+ if(!sortCol) return records;
3662
+ var type=colTypes[sortCol]||'string';
3663
+ return records.slice().sort(function(a,b){ return cmpValue(a[sortCol],b[sortCol],type)*sortDir; });
3664
+ }
3665
+ function applyFilter(records){
3666
+ if(!filterText) return records;
3667
+ var q=filterText.toLowerCase();
3668
+ return records.filter(function(r){
3669
+ return fields.some(function(f){
3670
+ var v=r[f.id];
3671
+ return v!==null&&v!==undefined&&String(v).toLowerCase().indexOf(q)!==-1;
3672
+ });
3673
+ });
3674
+ }
3675
+ function render(){
3676
+ var app=document.getElementById('app');
3677
+ filteredRecords=sortRecords(applyFilter(allRecords));
3678
+ var totalFiltered=filteredRecords.length;
3679
+ var totalPages=Math.ceil(totalFiltered/pageSize)||1;
3680
+ if(currentPage>totalPages) currentPage=totalPages;
3681
+ var start=(currentPage-1)*pageSize;
3682
+ var pageRecords=filteredRecords.slice(start,start+pageSize);
3683
+ var html='';
3684
+ html+='<div class="header">';
3685
+ html+='<span class="title">DataStore Table</span>';
3686
+ html+='<span class="badge">'+total.toLocaleString()+' total on server</span>';
3687
+ if(allRecords.length<total){
3688
+ html+='<span class="badge" style="background:#fff3cd;color:#856404">batch: '+allRecords.length.toLocaleString()+'</span>';
3689
+ }
3690
+ html+='</div>';
3691
+ var countLabel=(filterText&&totalFiltered<allRecords.length)
3692
+ ?totalFiltered.toLocaleString()+' match'
3693
+ :allRecords.length.toLocaleString()+' record'+(allRecords.length!==1?'s':'');
3694
+ html+='<div class="controls">';
3695
+ html+='<input class="filter-input" id="filter" type="text" placeholder="Filter all columns\\u2026" value="'+escHtml(filterText)+'">';
3696
+ html+='<select class="page-size-select" id="pageSz">';
3697
+ [10,25,50,100].forEach(function(n){
3698
+ html+='<option value="'+n+'"'+(n===pageSize?' selected':'')+'>'+n+' rows</option>';
3699
+ });
3700
+ html+='</select>';
3701
+ html+='<span class="count-info">'+countLabel+'</span></div>';
3702
+ html+='<div class="table-wrapper"><table><thead><tr>';
3703
+ fields.forEach(function(f){
3704
+ var cls=(sortCol===f.id)?(sortDir===1?' class="sort-asc"':' class="sort-desc"'):'';
3705
+ html+='<th'+cls+' data-col="'+escHtml(f.id)+'">'+escHtml(f.id)+'</th>';
3706
+ });
3707
+ html+='</tr></thead><tbody>';
3708
+ if(pageRecords.length===0){
3709
+ html+='<tr><td colspan="'+fields.length+'" class="state-msg">No records found</td></tr>';
3710
+ } else {
3711
+ pageRecords.forEach(function(rec){
3712
+ html+='<tr>';
3713
+ fields.forEach(function(f){
3714
+ var v=rec[f.id];
3715
+ if(v===null||v===undefined){
3716
+ html+='<td class="null-val">\\u2014</td>';
3717
+ } else {
3718
+ var s=String(v);
3719
+ var display=s.length>80?s.slice(0,77)+'\\u2026':s;
3720
+ html+='<td title="'+escHtml(s)+'">'+escHtml(display)+'</td>';
3721
+ }
3722
+ });
3723
+ html+='</tr>';
3724
+ });
3725
+ }
3726
+ html+='</tbody></table></div>';
3727
+ if(totalPages>1){
3728
+ html+='<div class="pagination">';
3729
+ html+='<button class="page-btn" id="prevBtn"'+(currentPage===1?' disabled':'')+'>\\u2039 Prev</button>';
3730
+ var pages=pagesToShow(currentPage,totalPages);
3731
+ var prev=null;
3732
+ pages.forEach(function(p){
3733
+ if(prev!==null&&p-prev>1) html+='<span class="ellipsis">\\u2026</span>';
3734
+ html+='<button class="page-btn'+(p===currentPage?' active':'')+'" data-page="'+p+'">'+p+'</button>';
3735
+ prev=p;
3736
+ });
3737
+ html+='<button class="page-btn" id="nextBtn"'+(currentPage===totalPages?' disabled':'')+'>Next \\u203a</button>';
3738
+ html+='</div>';
3739
+ }
3740
+ if(total>allRecords.length){
3741
+ html+='<div class="notice">Showing batch of '+allRecords.length.toLocaleString()+' records (server total: '+total.toLocaleString()+'). Re-run the tool with a different <code>offset</code> to access more records.</div>';
3742
+ }
3743
+ app.innerHTML=html;
3744
+ bindEvents();
3745
+ }
3746
+ function bindEvents(){
3747
+ var filterEl=document.getElementById('filter');
3748
+ if(filterEl) filterEl.addEventListener('input',function(e){ filterText=e.target.value; currentPage=1; render(); });
3749
+ var pageSzEl=document.getElementById('pageSz');
3750
+ if(pageSzEl) pageSzEl.addEventListener('change',function(e){ pageSize=parseInt(e.target.value,10); currentPage=1; render(); });
3751
+ document.querySelectorAll('th[data-col]').forEach(function(th){
3752
+ th.addEventListener('click',function(){
3753
+ var col=th.getAttribute('data-col');
3754
+ if(sortCol===col){ sortDir=-sortDir; } else { sortCol=col; sortDir=1; }
3755
+ render();
3756
+ });
3757
+ });
3758
+ var p=document.getElementById('prevBtn');
3759
+ if(p) p.addEventListener('click',function(){ currentPage--; render(); });
3760
+ var n=document.getElementById('nextBtn');
3761
+ if(n) n.addEventListener('click',function(){ currentPage++; render(); });
3762
+ document.querySelectorAll('.page-btn[data-page]').forEach(function(btn){
3763
+ btn.addEventListener('click',function(){ currentPage=parseInt(btn.getAttribute('data-page'),10); render(); });
3764
+ });
3765
+ }
3766
+ function initTable(data){
3767
+ fields=data.fields||[];
3768
+ allRecords=data.records||[];
3769
+ total=data.total||allRecords.length;
3770
+ colTypes=detectColTypes(allRecords,fields);
3771
+ currentPage=1; sortCol=null; sortDir=1; filterText='';
3772
+ render();
3773
+ }
3774
+ window.addEventListener('message',function(event){
3775
+ var msg=event.data;
3776
+ if(!msg||typeof msg!=='object') return;
3777
+ var data=
3778
+ (msg._meta&&msg._meta.ui&&msg._meta.ui.data)||
3779
+ (msg.toolResult&&msg.toolResult._meta&&msg.toolResult._meta.ui&&msg.toolResult._meta.ui.data)||
3780
+ (msg.result&&msg.result._meta&&msg.result._meta.ui&&msg.result._meta.ui.data)||
3781
+ (msg.fields&&msg.records?msg:null);
3782
+ if(data&&(data.fields||data.records)) initTable(data);
3783
+ });
3784
+ }());
3785
+ </script>
3786
+ </body>
3787
+ </html>`;
3788
+ function registerDatastoreTableUiResource(server2) {
3789
+ server2.registerResource(
3790
+ "ckan-datastore-table-ui",
3791
+ new ResourceTemplate5("ckan-ui://datastore-table", { list: void 0 }),
3792
+ {
3793
+ title: "CKAN DataStore Table Viewer",
3794
+ description: "Interactive sortable/filterable/paginated table for DataStore query results (MCP Apps UI)",
3795
+ mimeType: "text/html"
3796
+ },
3797
+ async (uri) => {
3798
+ return {
3799
+ contents: [
3800
+ {
3801
+ uri: uri.href,
3802
+ mimeType: "text/html",
3803
+ text: DATASTORE_TABLE_HTML
3804
+ }
3805
+ ]
3806
+ };
3807
+ }
3808
+ );
3809
+ }
3810
+
3446
3811
  // src/resources/index.ts
3447
3812
  function registerAllResources(server2) {
3448
3813
  registerDatasetResource(server2);
@@ -3452,6 +3817,7 @@ function registerAllResources(server2) {
3452
3817
  registerOrganizationDatasetsResource(server2);
3453
3818
  registerTagDatasetsResource(server2);
3454
3819
  registerFormatDatasetsResource(server2);
3820
+ registerDatastoreTableUiResource(server2);
3455
3821
  }
3456
3822
 
3457
3823
  // src/prompts/theme.ts
@@ -3725,7 +4091,7 @@ var registerAllPrompts = (server2) => {
3725
4091
  function createServer() {
3726
4092
  return new McpServer({
3727
4093
  name: "ckan-mcp-server",
3728
- version: "0.4.38"
4094
+ version: "0.4.40"
3729
4095
  });
3730
4096
  }
3731
4097
  function registerAll(server2) {