Kaggle MCP save_notebook silently drops all data-source fields (competition/dataset/kernel/model)
Kaggle MCP save_notebook silently drops all data-source fields (competition/dataset/kernel/model)
Summary
The hosted Kaggle MCP server (https://www.kaggle.com/mcp) exposes a
save_notebook tool. When you call it with any of the data-source arrays
(competitionDataSources, datasetDataSources, kernelDataSources,
modelDataSources, or their ...Setter variants), the call returns success
with a valid version_number/kernel_id, but the sources are never attached to
the notebook. The resulting kernel runs with an empty /kaggle/input, and the
committed metadata shows no attached sources.
The equivalent public REST endpoint — POST /api/v1/kernels/push, which the
kaggle-api CLI uses under the hood — accepts the same field names and attaches
the sources correctly. Both paths map to the same backend RPC
(ApiSaveKernelRequest → POST /api/v1/kernels/push), so the defect appears to be
in the MCP tool's field forwarding, not in the backend.
I'm filing here because kaggle-api owns the kernels push code path and the
ApiSaveKernelRequest proto that the MCP save_notebook tool wraps. If the MCP
server lives in a different (non-public) repo, please redirect — but the reproduction
and the "what works" contrast below should let the right team pinpoint it quickly.
Impact
For notebook-only / code competitions, this makes MCP save_notebook unusable
end-to-end: without the competition data mounted, the notebook cannot read the input
files, so it errors at runtime (FileNotFoundError on the competition data), never
produces submission.csv, and therefore cannot be submitted. Agentic/MCP workflows
that rely solely on the documented MCP tools hit a hard dead end here.
Environment
- Kaggle MCP server:
https://www.kaggle.com/mcp - Auth: token authentication via
Authorization: Bearer KGAT_...(not OAuth) - Transport: JSON-RPC over HTTP (
tools/list,tools/call) - MCP client: tested from an MCP-capable client and with hand-built
curlrequests directly against the endpoint (identical result — see below).
The save_notebook tool schema (from tools/list)
The tool advertises both a plain field and a ...Setter variant for every
data-source array, e.g. (abridged):
save_notebook.request:
slug: string
text: string
language: string
kernelType: string
kernelExecutionType: string
isPrivate: boolean
enableInternet: boolean
competitionDataSources: array|null
competitionDataSourcesSetter: array|null
datasetDataSources: array|null
datasetDataSourcesSetter: array|null
kernelDataSources: array|null
modelDataSources: array|null
...Reproduction
1) A minimal diagnostic notebook that just lists /kaggle/input
from pathlib import Path
for p in sorted(Path('/kaggle/input').rglob('*'))[:50]:
print('FOUND:', p)
print('DONE')2) Call MCP save_notebook with a competition (or dataset) source attached
tools/call payload (competition example; a public dataset like
kaggle/meta-kaggle reproduces the same way):
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "save_notebook",
"arguments": {
"request": {
"slug": "USER/diag-mount",
"newTitle": "Diag Mount",
"text": "from pathlib import Path\nfor p in sorted(Path('/kaggle/input').rglob('*'))[:50]:\n print('FOUND:', p)\nprint('DONE')\n",
"language": "python",
"kernelType": "script",
"isPrivate": true,
"enableInternet": false,
"kernelExecutionType": "SaveAndRunAll",
"competitionDataSources": ["<COMPETITION_SLUG>"]
}
}
}
}Result: the call succeeds:
{ "ref": "/code/USER/diag-mount", "url": "...", "version_number": N, "kernel_id": ... }…but the run log shows only:
DONEi.e. /kaggle/input is empty — nothing was mounted.
3) Variants tried (all fail identically)
competitionDataSources: [...]onlycompetitionDataSourcesSetter: [...]only- both
competitionDataSourcesandcompetitionDataSourcesSettertogether datasetDataSources: ["kaggle/meta-kaggle"](rules out anything competition-specific)- minimal payloads vs. fully-populated payloads
In every case, the notebook runs with an empty /kaggle/input, and
get_notebook_info for the committed version returns metadata with no
*_data_sources present.
4) Rule out client-side serialization
The same request was sent as a hand-built JSON array directly to the MCP
endpoint with curl (no MCP-client library in the path). The data source is still
dropped. This rules out client-side array serialization and points to the MCP
server handler.
What DOES work (the contrast that isolates the bug)
The classic public REST endpoint attaches the sources correctly with the same field name and the same token:
curl -s -X POST "https://www.kaggle.com/api/v1/kernels/push" \
-H "Authorization: Bearer KGAT_..." \
-H "Content-Type: application/json" \
--data '{
"slug": "USER/diag-mount",
"newTitle": "Diag Mount",
"text": "from pathlib import Path\nfor p in sorted(Path(\"/kaggle/input\").rglob(\"*\"))[:50]:\n print(\"FOUND:\", p)\nprint(\"DONE\")\n",
"language": "python",
"kernelType": "script",
"isPrivate": true,
"enableInternet": false,
"competitionDataSources": ["<COMPETITION_SLUG>"]
}'Response includes "invalidCompetitionSources": [] (source accepted), and after the
run the log shows the data mounted, e.g.:
FOUND: /kaggle/input/competitions/<COMPETITION_SLUG>/train_...
FOUND: /kaggle/input/competitions/<COMPETITION_SLUG>/test_...
FOUND: /kaggle/input/competitions/<COMPETITION_SLUG>/valid_...
DONEWhy this proves it's the MCP wrapper, not the backend
- In
kaggle-api,kernels_push()builds anApiSaveKernelRequestand setsrequest.competition_data_sourcesbefore callingsave_kernel(request). ApiSaveKernelRequest's own endpoint isPOST /api/v1/kernels/push(methodPOST) — i.e. the MCPsave_notebooktool and the CLIkernels pushtarget the same backend RPC and the same proto field (competition_data_sources).- Since the backend clearly honors the field when called via REST, the loss must
happen in the MCP tool's request construction/forwarding (e.g. the
...Settervs. plain field mapping not being applied to the outgoingApiSaveKernelRequest).
Additional working notes (useful for anyone hitting this)
These aren't bugs, but they surprised us and may help triage / help others:
- Two data-mount paths exist. Competition data can appear at
/kaggle/input/<slug>/or/kaggle/input/competitions/<slug>/. Code should locate files viaPath('/kaggle/input').rglob(name)rather than hard-coding a path. - First run after newly attaching a source may not mount it. Even via the
working REST path, the first run right after a source is added sometimes still
shows an empty
/kaggle/input; pushing the identical body a second time mounts it and the run succeeds. (Reported here in case it's related to the same attachment pipeline.) - The rest of the MCP competition flow works fine. Once a notebook version has
run to
COMPLETEand producedsubmission.csv, the MCP toolscreate_code_competition_submission(with the committedkernelVersion) andget_competition_submission(poll untilstatus: COMPLETE+public_score) work as documented. Onlysave_notebook's data-source attachment is broken. get_notebook_info/get_notebook_session_statusreturn "Not found" until a version has actually committed/completed (private drafts and failed-only kernels aren't queryable), which is easy to misread as an auth problem.
Expected behavior
MCP save_notebook should forward the provided data-source arrays to the backend so
that the resulting notebook mounts them under /kaggle/input, matching the behavior
of POST /api/v1/kernels/push / kaggle kernels push.
Actual behavior
MCP save_notebook accepts the data-source arrays, returns success, but produces a
notebook with no data sources attached and an empty /kaggle/input.
Suggested fix direction
Ensure the MCP save_notebook handler maps its *DataSources / *DataSourcesSetter
tool arguments onto the corresponding repeated fields of the outgoing
ApiSaveKernelRequest (competition_data_sources, dataset_data_sources,
kernel_data_sources, model_data_sources) before dispatching to
POST /api/v1/kernels/push. A round-trip check (call save_notebook, then read the
committed metadata and assert the sources are present) would catch regressions.
Source: Kaggle/kaggle-cli