MCP Server
KUKAN provides a Model Context Protocol (MCP) compliant server endpoint. This enables Claude, ChatGPT, and other MCP-compatible AI agents to search and browse the data catalog.
Endpoint
Section titled “Endpoint”POST /api/mcpOperates in stateless mode using the Streamable HTTP transport.
Connecting
Section titled “Connecting”Claude Code
Section titled “Claude Code”Add it with the CLI:
claude mcp add --transport http kukan https://your-kukan-site/api/mcpTo write it in a project’s .mcp.json, set type to http:
{ "mcpServers": { "kukan": { "type": "http", "url": "https://your-kukan-site/api/mcp" } }}To include private datasets of your organizations, pass an API token as a header (--header "Authorization: Bearer kukan_..." on the CLI, or "headers": { "Authorization": "Bearer kukan_..." } in .mcp.json).
Claude Desktop / claude.ai
Section titled “Claude Desktop / claude.ai”Add https://your-kukan-site/api/mcp as a custom connector under Connectors in the settings (see the Claude Help Center for which plans allow it).
- The connection comes from Anthropic’s cloud, not from your computer. A site the internet cannot reach (a closed network, behind a VPN) cannot be connected. Use a client that connects from your computer, such as Claude Code, instead
- A custom connector cannot carry an API token header, so it reaches public datasets only
Testing with curl
Section titled “Testing with curl”curl -X POST https://your-kukan-site/api/mcp \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-03-26", "capabilities": {}, "clientInfo": { "name": "test", "version": "0.1" } } }'Guidance on Connecting
Section titled “Guidance on Connecting”The instructions of the initialize result tell the client how the tools go together. MCP clients add them to what the model is told.
- Language to search in: search in the catalog’s language and answer in the user’s. The catalog’s language is judged from the characters of the public datasets’ titles (the 50 most recently updated). Only Japanese and Korean are named. A catalog of kanji alone (which cannot tell Japanese from Chinese), of a language in Latin script, or of mixed languages gets no language named — only “in the language of the titles the searches return”
- Choosing the words: give a few words a dataset’s title or a document would use, not the whole question; if nothing fits, drop a word or try another
- Which tools go together: where the answer is in documents’ text,
search_passagesthenget_document_section; where it is a figure in a table,search_datasetsthenget_resource_schemaandquery_resource - Citing: cite the resource, the page or rows, and the URL each tool gives. Judging what the text means is left to the caller
Available Tools
Section titled “Available Tools”All tools are read-only. Access to private datasets is controlled based on the authenticated user’s permissions.
| Tool | Description |
|---|---|
search_datasets |
Search datasets by keyword or natural language (hybrid search). Also says which file matched |
get_dataset |
Get detailed dataset information including resource list |
get_resource |
Get resource (file) metadata, URL, and its AI-written description |
get_resource_schema |
Get a resource’s column schema (names, types, row count, stats) and whether OData serves it. Tables only |
query_resource |
Run a read-only SQL query (SELECT/WITH only) over a resource’s data. Tables only |
get_document_outline |
Get a document’s outline: its headings with their pages, or a workbook’s sheets and rows. PDF/Word/PowerPoint/Excel/Ichitaro/Markdown |
get_document_section |
Read a document’s text by page, slide, sheet or heading, with where it is from. PDF/Word/PowerPoint/Excel/Ichitaro/Markdown |
search_passages |
Find the passages of documents’ text that match a query, with where each is from and its page. PDF/Word/PowerPoint/Excel/Ichitaro/Markdown |
list_organizations |
List organizations |
list_groups |
List groups (categories) |
list_tags |
List tags |
Tool Details
Section titled “Tool Details”search_datasets
Section titled “search_datasets”Search the data catalog. Besides keywords, natural-language queries work too —
semantically related datasets are included in the results (hybrid search).
By keyword, a dataset matches if it holds every word of the query, so a few words
its title or description would use find more than the whole question. Where no dataset holds every
word, the datasets holding fewer of them are returned, the reply says so first, and each names the
words it lacks under Lacks:.
When a dataset holds several files, the result says which of them matched, under
Matched files:, each with what the match is on account of — in its name or description, in its contents, in its AI-written description, or by meaning 0.42
(the number is the similarity). A match by meaning means the query’s words appear
nowhere in that file, so do not go looking for them there. The first five are named
and the rest are counted. Each result also carries its organization, description (first
200 characters), and formats (read from the database).
| Parameter | Type | Required | Description |
|---|---|---|---|
q |
string | Yes | Search keywords or a natural-language query |
organization |
string | No | Filter by organization name (slug) |
tags |
string[] | No | Filter by tag names |
semantic |
boolean | No | false limits results to keyword matches only (default: true) |
offset |
number | No | Number of results to skip (default: 0) |
limit |
number | No | Maximum results (default: 10, max: 50) |
get_dataset
Section titled “get_dataset”Get detailed information about a dataset. Resources are listed in the order of the public
page, with a [section name] heading line where a section begins. / nesting is indented
to the same depth as on the public page, and the resources under a heading one level deeper.
| Parameter | Type | Required | Description |
|---|---|---|---|
nameOrId |
string | Yes | Dataset name (slug) or UUID |
get_resource
Section titled “get_resource”Get resource metadata. Returns the download URL for uploaded files. A resource inside a
section carries a Section: line.
A document whose text has been extracted (PDF, Word, PowerPoint, Excel, Ichitaro, the Office formats
before 2007 included) carries a
Document text: line saying it can be read with get_document_outline and
get_document_section (or, for a document of scans, that it has none to read).
Where the site has AI-written descriptions
enabled, one is appended, together with how much of the file it was written from
(read the column layout and the first 100 of 1200 rows, for example) and which version
it describes. If the file has been replaced and the description has not caught up, that
reads describes v1, and the file is now v2, so an old description is never mistaken for a
current one. Where none was written, the reason is given instead.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Resource UUID |
get_resource_schema
Section titled “get_resource_schema”Get a resource’s column schema (names, types, nullability, row count, and min/max for
numeric columns). When the resource has a settled primary key it is reported on a
Primary key: line. Each column carries its role (name, category, free text, identifier,
number or date, number written as text), with the most frequent values of a name or category
column and the values that do not read as numbers in a column of numbers written as text. Only
tables — CSV/TSV, and JSON/GeoJSON whose records read as one — with a generated preview are
queryable. When the
resource is queryable, the output names the query table data and includes an example query,
so the agent can write SQL after inspecting the schema; when it is not (the preview is gone
or does not reflect the current content), the columns are still listed for reference but the
query guidance is withheld.
Whether the table is served over OData comes back too. When it is, the output carries the
feed URL (ending in /Rows), what identifies its rows — the primary key, or the RowId column
KUKAN adds and why it had to — and, for a table whose rows are large enough that a page may
fail to read, a caution saying so. When it is not, it carries the reason: headings that are not OData
identifiers, two columns of one name, a private or draft dataset. Queryable over SQL and
readable over OData are not the same thing, so read this line before choosing how to fetch
the data.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Resource UUID |
query_resource
Section titled “query_resource”Run a read-only SQL query (SELECT / WITH only) over a resource’s data and return the
result as a Markdown table. The data is exposed as a table named data. Use
get_resource_schema first to learn the column names and types.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Resource UUID |
sql |
string | Yes | A single SELECT/WITH statement over the data table |
Runs in a throwaway in-memory DuckDB instance with external access disabled and row/byte/time
limits. Double-quote column names that contain non-ASCII characters. See the REST
/resources/{id}/query endpoint for limits and error details.
get_document_outline
Section titled “get_document_outline”Get a document’s outline, to choose what to read before calling get_document_section.
- Headings: the PDF’s bookmarks (or where it has none, its heading tags), the Word file’s heading
styles or the slides’ titles, indented by level, each with the pages (slides) it spans —
pages 15–18 1-1-3 Staff - Workbooks: the sheets in place of headings, with their names and rows
- Pages and unreadable pages: the range of pages (slides, sheets), the pages with no text to read (scans), and those with some characters that cannot be read (a PDF font that does not map them), which are left out of their text
- The response opens with the same source as
get_document_section
A Word file from before 2007 (DOC) and an Ichitaro file mark no headings a reader can find, so their text is one block. An Ichitaro file of several sheets is a block a sheet, selected by the sheet’s number.
A document that is all scans, with no text, still has its pages, headings and unreadable pages listed.
A long outline is cut at about 12,000 characters (a heading at 120, the list of unreadable pages at a
quarter of it), with a count of what was left out; give from and to to see part of it.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Resource UUID |
from |
number | No | First page, slide or sheet (1-based) |
to |
number | No | Last page, slide or sheet |
get_document_section
Section titled “get_document_section”Read the text KUKAN extracted from a document (PDF, Word, PowerPoint, Excel, Ichitaro, the Office formats before 2007 included) by where it is in the file: a range of pages — a PDF’s pages, a presentation’s slides, a workbook’s sheets — a heading, or both. No AI model is called; what comes back is the file’s own text.
The response opens with the source to cite: the resource and dataset names, the resource page and
file URLs (for a PDF, add #page=N to open it at a page), the format and the version. Each block of
text is labelled with its page (and printed page number, where it differs), slide, or sheet and
rows, and the headings it is under — --- page 12 (iv) · Chapter 2 > Population. A page with no
text to read (a scan) says so rather than being left out, and one with characters that cannot be
read says how many were left out of its text. If the file has been replaced and its text
not yet extracted again, the response gives the version the text is from (Text from: version 1)
and the file’s current version (Current file version: 2) in place of Version:; cite the version
the text is from.
A heading matches when it contains the text given, ignoring case, width and spaces; it also matches a
sheet’s name. A Word file has no pages until it is laid out, so it is selected by heading only. When
Joining headings with >, as in Chapter 1 > Overview, picks a heading under another. When nothing
matches, the response gives the document’s page range and points to get_document_outline.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Resource UUID |
from |
number | No | First page, slide or sheet (1-based) |
to |
number | No | Last page, slide or sheet (inclusive) |
heading |
string | No | Part of a heading the text is under, or of a sheet’s name |
max_chars |
number | No | Characters of text to return (default: 8,000, max: 32,000) |
cursor |
string | No | The cursor a previous response ended with; from, to and heading come from it |
A longer section is cut — at a line break where it can be — and the response ends with a cursor to read on. A cursor stops working once the document is extracted again.
The outline and the text are also available over REST as GET /api/v1/resources/{id}/document-outline and document-section, with the same parameters and a JSON response.
search_passages
Section titled “search_passages”Return the passages of documents’ text across the catalog (PDF, Word, PowerPoint, Excel, Ichitaro) that
match a query, best first. Datasets and resources are found first, by the same search as
search_datasets, and then the matching passages inside their documents. Where no dataset holds every
word, the documents are found by fewer of them, as search_datasets does, and the reply says so first. No generative AI is called
(the query is embedded, to find documents by meaning as well; a site without an embedding model finds
them by words alone). Tables such as CSV are not searched (use get_resource_schema and query_resource).
Each passage opens with the same source as get_document_section (a PDF’s file URL comes with
#page=N for the passage’s page), then where it is (page, slide or sheet, and the headings above it),
the words that matched, and its text. Each comes with a cursor: give it to get_document_section to read
around the passage from its page. A document without pages (Word) is read from where the passage is.
Passages inside a document are matched by words, not meaning. If nothing fits, search again in the words a document would use (an administration’s terms, for instance). Where more passages matched than were returned, the response ends by saying so.
| Parameter | Type | Required | Description |
|---|---|---|---|
q |
string | Yes | Words to search for |
organization |
string | No | Filter by organization name (slug) |
tags |
string[] | No | Filter by tag names |
k |
number | No | Passages to return (default: 5, max: 20) |
per_resource |
number | No | Passages from one resource at most (default: 2) |
max_chars |
number | No | Characters of text to return, all passages together (default: 8,000, max: 32,000) |
The same search is available over REST as GET /api/v1/passages, with the same parameters, and semantic=false to match by words alone.
list_organizations
Section titled “list_organizations”| Parameter | Type | Required | Description |
|---|---|---|---|
q |
string | No | Search by name |
offset |
number | No | Number of results to skip (default: 0) |
limit |
number | No | Maximum results (default: 20, max: 100) |
list_groups
Section titled “list_groups”| Parameter | Type | Required | Description |
|---|---|---|---|
q |
string | No | Search by name |
offset |
number | No | Number of results to skip (default: 0) |
limit |
number | No | Maximum results (default: 20, max: 100) |
list_tags
Section titled “list_tags”| Parameter | Type | Required | Description |
|---|---|---|---|
q |
string | No | Search by name |
offset |
number | No | Number of results to skip (default: 0) |
limit |
number | No | Maximum results (default: 50, max: 200) |
Authentication
Section titled “Authentication”The MCP endpoint integrates with KUKAN’s authentication system.
- Unauthenticated: Access to public datasets only
- Authenticated: Access to private datasets within your organizations
- System admin: Access to all datasets
To authenticate, send an API token in the Authorization: Bearer kukan_... header (see Authentication).
Technical Specifications
Section titled “Technical Specifications”- Protocol: MCP (Model Context Protocol). The version is negotiated with the client at initialization (2024-11-05 and later)
- Transport: Streamable HTTP
- Mode: Stateless (no session management)
- Response format: JSON-RPC 2.0