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
curl
requests 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:
i.e. /kaggle/input is empty — nothing was mounted.
3) Variants tried (all fail identically)
competitionDataSources: [...] only
competitionDataSourcesSetter: [...] only
- both
competitionDataSources and competitionDataSourcesSetter together
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_...
DONE
Why this proves it's the MCP wrapper, not the backend
- In
kaggle-api, kernels_push() builds an ApiSaveKernelRequest and sets
request.competition_data_sources before calling save_kernel(request).
ApiSaveKernelRequest's own endpoint is POST /api/v1/kernels/push (method
POST) — i.e. the MCP save_notebook tool and the CLI kernels push target 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
...Setter vs. plain field mapping not being applied to the outgoing
ApiSaveKernelRequest).
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 via Path('/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 COMPLETE and produced submission.csv, the MCP tools
create_code_competition_submission (with the committed kernelVersion) and
get_competition_submission (poll until status: COMPLETE + public_score)
work as documented. Only save_notebook's data-source attachment is broken.
get_notebook_info / get_notebook_session_status return "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.
Kaggle MCP
save_notebooksilently drops all data-source fields (competition/dataset/kernel/model)Summary
The hosted Kaggle MCP server (
https://www.kaggle.com/mcp) exposes asave_notebooktool. When you call it with any of the data-source arrays(
competitionDataSources,datasetDataSources,kernelDataSources,modelDataSources, or their...Settervariants), the call returns successwith a valid
version_number/kernel_id, but the sources are never attached tothe notebook. The resulting kernel runs with an empty
/kaggle/input, and thecommitted metadata shows no attached sources.
The equivalent public REST endpoint —
POST /api/v1/kernels/push, which thekaggle-apiCLI uses under the hood — accepts the same field names and attachesthe sources correctly. Both paths map to the same backend RPC
(
ApiSaveKernelRequest→POST /api/v1/kernels/push), so the defect appears to bein the MCP tool's field forwarding, not in the backend.
I'm filing here because
kaggle-apiowns thekernels pushcode path and theApiSaveKernelRequestproto that the MCPsave_notebooktool wraps. If the MCPserver 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_notebookunusableend-to-end: without the competition data mounted, the notebook cannot read the input
files, so it errors at runtime (
FileNotFoundErroron the competition data), neverproduces
submission.csv, and therefore cannot be submitted. Agentic/MCP workflowsthat rely solely on the documented MCP tools hit a hard dead end here.
Environment
https://www.kaggle.com/mcpAuthorization: Bearer KGAT_...(not OAuth)tools/list,tools/call)curlrequests directly against the endpoint (identical result — see below).
The
save_notebooktool schema (fromtools/list)The tool advertises both a plain field and a
...Settervariant for everydata-source array, e.g. (abridged):
Reproduction
1) A minimal diagnostic notebook that just lists
/kaggle/input2) Call MCP
save_notebookwith a competition (or dataset) source attachedtools/callpayload (competition example; a public dataset likekaggle/meta-kagglereproduces 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:
i.e.
/kaggle/inputis empty — nothing was mounted.3) Variants tried (all fail identically)
competitionDataSources: [...]onlycompetitionDataSourcesSetter: [...]onlycompetitionDataSourcesandcompetitionDataSourcesSettertogetherdatasetDataSources: ["kaggle/meta-kaggle"](rules out anything competition-specific)In every case, the notebook runs with an empty
/kaggle/input, andget_notebook_infofor the committed version returns metadata with no*_data_sourcespresent.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 stilldropped. 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:
Response includes
"invalidCompetitionSources": [](source accepted), and after therun the log shows the data mounted, e.g.:
Why this proves it's the MCP wrapper, not the backend
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 thesame backend RPC and the same proto field (
competition_data_sources).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:
/kaggle/input/<slug>/or/kaggle/input/competitions/<slug>/. Code shouldlocate files via
Path('/kaggle/input').rglob(name)rather than hard-coding a path.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 itand the run succeeds. (Reported here in case it's related to the same attachment
pipeline.)
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. Only
save_notebook's data-source attachment is broken.get_notebook_info/get_notebook_session_statusreturn "Not found" until aversion 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_notebookshould forward the provided data-source arrays to the backend sothat the resulting notebook mounts them under
/kaggle/input, matching the behaviorof
POST /api/v1/kernels/push/kaggle kernels push.Actual behavior
MCP
save_notebookaccepts the data-source arrays, returns success, but produces anotebook with no data sources attached and an empty
/kaggle/input.Suggested fix direction
Ensure the MCP
save_notebookhandler maps its*DataSources/*DataSourcesSettertool arguments onto the corresponding repeated fields of the outgoing
ApiSaveKernelRequest(competition_data_sources,dataset_data_sources,kernel_data_sources,model_data_sources) before dispatching toPOST /api/v1/kernels/push. A round-trip check (callsave_notebook, then read thecommitted metadata and assert the sources are present) would catch regressions.