Skip to content

Kaggle MCP save_notebook silently drops all data-source fields (competition/dataset/kernel/model) #1140

Description

@adivekar-utexas

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
(ApiSaveKernelRequestPOST /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:

DONE

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions