feat(@deepnote/mcp): v1 by jamesbhobbs · Pull Request #255 · deepnote/deepnote · GitHub
Skip to content

feat(@deepnote/mcp): v1 - #255

Closed
jamesbhobbs wants to merge 44 commits into
mainfrom
mcp-skills
Closed

jamesbhobbs wants to merge 44 commits into
mainfrom
mcp-skills

Conversation

@jamesbhobbs

@jamesbhobbs jamesbhobbs commented Jan 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced MCP (Model Context Protocol) server integration for seamless AI assistant connectivity
    • Added comprehensive toolset: notebook reading/analysis, writing/editing, format conversion (Jupyter, Quarto, Python), execution, and snapshot management
    • Implemented guided prompt system for common notebook workflows: creation, enhancement, documentation, and best practices
    • Added resource management for workspace file discovery and access
  • Documentation

    • Added MCP server installation and usage guide with Cursor integration steps
    • Created example notebooks demonstrating real-world workflows: A/B testing, API analytics, churn prediction, ETL pipelines, and financial dashboards
  • Tests

    • Added comprehensive test coverage for tools, prompts, resources, and execution functionality

jamesbhobbs and others added 11 commits January 28, 2026 13:21
Replace direct console.log() and chalk imports across CLI commands with
centralized output utilities (output(), log(), getChalk()) from output.ts.
This ensures --no-color, --debug, and --quiet flags are consistently
respected by all commands.

Files updated:
- stats, dag, lint, run, open, inspect, diff, convert commands
- output-renderer.ts, utils/metrics.ts
- cli.ts (outputError callback)

Added tests verifying --no-color and --quiet behavior in stats, dag, and
lint commands.
- Extend `run` command to accept .ipynb, .py (percent/marimo), and .qmd
  files with automatic conversion to DeepnoteFile before execution
- Add `--open` flag to `run` command to open project in Deepnote Cloud
  after successful execution
- Add `--open` flag to `convert` command to open converted .deepnote
  file in Deepnote Cloud

New utilities:
- format-converter.ts: resolves and converts any supported format
- open-in-cloud.ts: reusable upload and browser open logic

Includes 31 new tests covering format conversion, multi-format run,
and integration tests for .ipynb, .py, and .qmd execution.
- Add shared analysis utilities (analysis.ts) extracting reusable
  functions from lint.ts and stats.ts for stats, lint, and DAG analysis
- Add new `analyze` command for comprehensive project analysis including
  quality score, structure info, dependency analysis, and suggestions
- Add `--context` flag to run command that includes analysis info
  (stats, lint issues, variable definitions/usages) in output
- Add auto-diagnosis on failure: when JSON/TOON output is enabled and
  execution fails, automatically include upstream dependencies, related
  lint issues, and used variables to help LLMs understand failures
- Refactor lint.ts and stats.ts to use shared analysis utilities,
  reducing code duplication by ~550 lines
Comment thread packages/mcp/README.md

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before publishing use

"deepnote": {
  "command": "node",
  "args": ["/Users/james/dev/deepnote/deepnote/packages/mcp/dist/bin.js"]
},

@coderabbitai

coderabbitai Bot commented Jan 28, 2026 •

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Establishes a complete MCP server implementation at packages/mcp/. Introduces package manifest, CLI entry point, and modular tool handlers across five categories: reading (structure/linting/DAG analysis), writing (notebook/block creation and mutation), conversion (format translation), execution (notebook runtime), and snapshots (output persistence). Implements prompt system with six MCP prompts for common notebook workflows, resource management for workspace and example files, and server scaffold that wires tools, prompts, resources, and instructions. Includes comprehensive unit tests and example notebooks demonstrating capabilities. No functional changes to existing code; purely additive.

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.68% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(@deepnote/mcp): v1' accurately reflects the main change—introducing the MCP (Model Context Protocol) server package as a new versioned feature, covering README, package.json, CLI entry points, server scaffolding, tools (reading, writing, conversion, execution, snapshots), and example notebooks.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@jamesbhobbs jamesbhobbs changed the title Mcp skills feat(@deepnote/mcp): v1 Jan 28, 2026
@codecov

codecov Bot commented Jan 28, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 40.73320% with 582 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.80%. Comparing base (6eeeec0) to head (3b4dca7).

Files with missing lines Patch % Lines
packages/mcp/src/tools/writing.ts 15.93% 153 Missing ⚠️
packages/mcp/src/tools/execution.ts 35.00% 117 Missing ⚠️
packages/mcp/src/tools/reading.ts 51.13% 86 Missing ⚠️
packages/mcp/src/tools/conversion.ts 1.53% 64 Missing ⚠️
packages/mcp/src/test-tool.ts 0.00% 61 Missing ⚠️
packages/mcp/src/server.ts 0.00% 51 Missing ⚠️
packages/mcp/src/tools/snapshots.ts 77.09% 30 Missing ⚠️
packages/mcp/src/resources.ts 86.84% 10 Missing ⚠️
packages/mcp/src/prompts.ts 85.29% 5 Missing ⚠️
packages/mcp/src/bin.ts 0.00% 2 Missing ⚠️
... and 2 more

❌ Your patch status has failed because the patch coverage (40.73%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #255      +/-   ##
==========================================
- Coverage   92.13%   83.80%   -8.33%     
==========================================
  Files          85       97      +12     
  Lines        5083     6065     +982     
  Branches     1371     1629     +258     
==========================================
+ Hits         4683     5083     +400     
- Misses        400      982     +582     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

* docs(examples): mcp generated notebooks

* fix: inputs handling by mcp

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 27

🤖 Fix all issues with AI agents
In `@packages/mcp/package.json`:
- Around line 45-51: Add the token "modelcontextprotocol" to the cspell
dictionary by updating the cspell.json "words" array so the dependency
"@modelcontextprotocol/sdk" in package.json won't be flagged; locate cspell.json
and append "modelcontextprotocol" to the existing words list to allow that
package name.

In `@packages/mcp/README.md`:
- Line 167: Add the term "pytest" to the project's cspell dictionary so the
spellchecker no longer flags it: update the cspell configuration (e.g.,
cspell.json or .cspellrc) to include "pytest" in the "words" array or custom
dictionary, then commit the change so CI recognizes the term and the pipeline
stops failing on this common testing term.
- Line 9: The CSpell failure is caused by the word "toolsets" in the README line
"**Bounded toolsets**": either add "toolsets" to the cspell dictionary (e.g.,
add "toolsets" to your cspell.json word list) or edit the README to rephrase the
phrase to "Bounded tool sets" (two words) so the spell checker no longer errors;
update the README.md line accordingly or update the project's cspell config to
include the new word.
- Around line 124-128: Multiple fenced code blocks in the README (the examples
starting with "Use deepnote_scaffold with:" and the similar blocks later) lack a
language identifier, triggering MD040; update each fenced block by adding a
language tag such as text or plaintext (e.g., change ``` to ```text) for the
examples at the occurrences containing the phrases "Use deepnote_scaffold with:"
and the other similar example blocks so Markdownlint no longer flags them.

In `@packages/mcp/src/resources.ts`:
- Around line 13-19: The type BlobResourceContents is defined but never used;
either remove the BlobResourceContents declaration and update the union type
alias ResourceContents to only reference TextResourceContents, or if it's
intended for future use, add a clear TODO comment above BlobResourceContents
indicating planned usage. Also scan for and update any references to
ResourceContents or related types (e.g., TextResourceContents) to ensure the
union remains correct after removing or preserving BlobResourceContents.
- Around line 44-46: The empty catch block in resources.ts silently swallows
filesystem errors; change the catch to capture the exception (e.g., catch (err))
and either log the error with context (include the directory path/operation and
err) using the existing logger or surface a partial-result + error object to the
caller; update the block that currently reads "catch { // Directory not
accessible, skip }" to log or return error details so failures are visible
during debugging.
- Line 4: The CI spellcheck flags the import specifier '@modelcontextprotocol'
in packages/mcp/src/resources.ts; update your repository CSpell dictionary
(e.g., .cspell.json or cspell.words referenced by your config) to include the
token "modelcontextprotocol" (or the full module name) so the import string is
accepted; locate the CSpell config file used by the pipeline and add
"modelcontextprotocol" to the "words" array (matching the change you made for
server.ts) and commit that change.
- Around line 92-102: The current code pushes resources using the absolute
filesystem path (variable file) into the URI, leaking system structure; change
the URI construction in the loop that iterates workspaceFiles to use the
computed relativePath (e.g., uri: `deepnote://file/${relativePath}`) instead of
`file`, and update the corresponding readResource function to resolve that
relative path against workspaceRoot when opening/reading files so resource
lookup still points to the correct absolute file. Ensure references to
DEEPNOTE_EXTENSION, name and mimeType remain unchanged and that readResource
correctly normalizes/joins the relative path to workspaceRoot before filesystem
access.
- Around line 61-65: The current URI parsing function that returns {type:
'file', path: ...} for any non-matching deepnote:// value will misclassify
malformed URIs; change that parser so it returns an explicit error/unknown
result (e.g., { type: 'unknown', uri }) or throws an Error when the URI doesn't
match known patterns instead of defaulting to file, and then update readResource
to detect this 'unknown' result and handle it by throwing a clear error or
returning a rejected Promise with a descriptive message (use the parser function
name and readResource to locate the changes).
- Line 116: The current examplesDir assignment uses __dirname which is undefined
in ESM; update the code that sets examplesDir (the constant named examplesDir
where path.resolve is used) to derive the directory from import.meta.url via
fileURLToPath (import fileURLToPath from 'url') and path.dirname, or
alternatively make the examples path configurable and read it from env/config;
ensure you replace all references to __dirname and keep the resolved path logic
using path.resolve(path.dirname(fileURLToPath(import.meta.url)),
'../../../examples') or a configurable fallback.
- Around line 191-233: The code reads parsed.path directly via fs.readFile
(inside the block handling parsed.type === 'file') which allows path traversal;
fix by resolving and normalizing the input and ensuring the resolved absolute
path is inside the intended workspace root before reading: compute an
absolutePath from parsed.path (using path.resolve with the workspaceRoot),
verify absolutePath.startsWith(workspaceRoot) (or use path.relative and ensure
it does not start with '..'), and if the check fails return the same JSON error
response instead of calling fs.readFile and deserializeDeepnoteFile; keep
existing error handling and use the same uri/mimeType response shape when
rejecting.

In `@packages/mcp/src/server.ts`:
- Around line 1-10: The spellchecker flags the '@modelcontextprotocol'
identifier as unknown; add "modelcontextprotocol" (and optionally
"@modelcontextprotocol") to the project's cspell dictionary (e.g., cspell.json
"words" or cspell section in package.json) so imports like Server and
StdioServerTransport from '@modelcontextprotocol/sdk/...' are accepted; update
the config, commit the change, and re-run the pipeline.
- Around line 146-150: startServer currently creates the server and connects the
StdioServerTransport but doesn't handle process shutdown; add signal handlers
for SIGINT and SIGTERM that perform graceful cleanup by calling
server.disconnect() (or server.close() if that's the API) and disposing/closing
the StdioServerTransport instance, and ensure the handlers remove themselves
after running to avoid duplicate execution; locate startServer, createServer,
StdioServerTransport and the server.connect call to add this shutdown logic and
ensure startServer awaits the disconnect/cleanup before exiting.
- Around line 84-128: The prefix-based startsWith routing in server.ts is
fragile and repetitive; replace the long startsWith chains with an explicit
name-to-handler registry: build a Map of exact tool names to handler categories
(e.g., populate from your magicTools/readingTools/writingTools lists or from an
allTools array that includes a category), then lookup the incoming name and
dispatch to the appropriate handler function (handleMagicTool,
handleReadingTool, handleWritingTool, handleConversionTool, handleExecutionTool)
using that map; this ensures exact matching, avoids prefix collisions (e.g.,
deepnote_fix vs deepnote_fix_all), and centralizes routing logic for easier
maintenance.
- Around line 55-63: The request handler for GetPromptRequestSchema catches
errors and rethrows a new Error which discards the original stack; update the
handler (the async callback passed to server.setRequestHandler for
GetPromptRequestSchema that calls getPrompt) to either rethrow the caught error
directly (throw error) or rethrow preserving cause (throw new Error(message, {
cause: error })) so the original stack is retained; keep the existing message
extraction (message) if you choose cause, and ensure the thrown value is the
original error or wraps it via the cause option.
- Around line 21-26: The module-level constant workspaceRoot is computed at
import time (const workspaceRoot = process.env.DEEPNOTE_WORKSPACE ||
process.cwd()), which prevents updates for tests or runtime changes; remove that
top-level evaluation and instead compute workspaceRoot inside createServer() or
add a createServer(workspaceRoot?: string) parameter with a default of
process.env.DEEPNOTE_WORKSPACE || process.cwd(); update all usages in this file
to reference the local workspaceRoot variable (or the passed parameter) rather
than the removed module-level symbol so tests can inject isolated values and
runtime env changes take effect.

In `@packages/mcp/src/tools/conversion.ts`:
- Around line 118-123: The current computation of finalOutputPath and
finalProjectName uses a case-sensitive regex on absoluteInput which can fail for
uppercase extensions or extensionless files and may overwrite the source; update
the logic in conversion.ts to use path.parse on absoluteInput and always replace
or append the .deepnote extension when outputPath is not provided, and derive
finalProjectName from the parsed name when projectName is not provided; ensure
you use the existing variables finalOutputPath, finalProjectName, absoluteInput,
outputPath, and projectName when implementing this change.

In `@packages/mcp/src/tools/execution.ts`:
- Around line 210-217: The current loop uses b.id.startsWith(blockId) which can
match multiple blocks; update the lookup so you first try an exact match (b.id
=== blockId) across file.project.notebooks and use that if found; if no exact
match and you must support prefix lookup, collect all blocks where
b.id.startsWith(blockId) and only assign targetBlock/targetNotebook when that
collection has exactly one element, otherwise raise/return an explicit error or
validation message indicating ambiguity so callers can disambiguate; ensure you
update any logic that assumed a single match to handle the error case.

In `@packages/mcp/src/tools/magic.ts`:
- Around line 2112-2254: handleRefactor currently copies
extractedFunctions/extractedClasses into a new module notebook but leaves the
original cells intact and doesn't add an import; update handleRefactor to (1)
remove the extracted function/class source lines from their original Deepnote
blocks (use extractedFunctions[*].blockId and extractedClasses[*].blockId to
locate blocks and modify block.content) so the original definitions are deleted
or replaced with a short placeholder, (2) insert a new import code block into
the original notebook(s) (use createBlock to create a code cell that does either
"from <moduleName> import ..." or "import <moduleName> as <alias>" listing the
extracted names), and (3) ensure you persist these changes by calling
saveDeepnoteFile(filePath, file) after mutating the original notebooks; keep
existing module creation (moduleId, moduleBlocks) but perform the mutation+save
before final return.
- Around line 892-1053: The handleEnhance function currently only mutates
notebook.blocks for documentation while inputs/visualizations/structure only
append to the changes array but are never applied; either implement their
mutations or mark them as suggestions and not count them as applied—do the
latter now: create a separate suggestions array (or add an applied: false flag)
for non-documentation findings (inputs, visualizations, structure), stop
mutating notebook.blocks for those, and change the final returned payload and
changesApplied computation so only actually applied changes (those that caused
notebook.blocks modifications via blocksToInsert and the sortingKey update) are
counted; adjust the places that push into changes (refer to the patterns loop,
the visualization check, and the structure header detection) to push into
suggestions or set applied:false, and ensure saveDeepnoteFile and the returned
JSON use changesApplied based on applied changes only.
- Around line 1148-1151: The current circular-dependency branch is a no-op;
replace it with real handling: when (fixAll || fixTypes.includes('circular'))
call a detection routine (e.g., detectCircularDependencies(projectGraph)) and if
cycles are found either attempt to resolve them via a reordering/breaking helper
(e.g., breakCircularDependency or topologicalSortFallback) or immediately fail
with a clear error (throw new Error('circular fix requested but not implemented;
found cycles: ' + JSON.stringify(cycles))). Ensure you reference the existing
variables fixAll and fixTypes and add/implement detectCircularDependencies and
breakCircularDependency helpers or, at minimum, throw the error so the request
is not silently ignored.
- Around line 2511-2528: The code unconditionally calls JSON.parse on resultText
(twice), causing non-JSON outputs (e.g., markdown from deepnote_explain) to
throw and mark steps failed; change the logic to attempt JSON.parse once inside
the existing try/catch (assign to a local parsed variable), use that parsed
value for lastOutputPath and for the results.push entry, and if parsing fails
set the result field to the raw resultText (not undefined) so non-JSON outputs
are preserved; update references to resultText, parsed, lastOutputPath, and the
results.push creation accordingly so parsing errors do not bubble up.
- Around line 1395-1404: The documentation suggestion logic in the function
using suggestAll/categories and the block check (block.type === 'code' &&
(!prevBlock || !prevBlock.type.includes('text-cell'))) treats only 'text-cell'
as valid preceding documentation, causing false positives for markdown; update
the condition that checks prevBlock.type (referenced where block.type,
prevBlock, and suggestions.push are used) to accept markdown/documentation block
types (e.g., 'markdown', 'text-cell', or any other project-specific doc types)
by testing for those values instead of only includes('text-cell'), so a
preceding markdown block prevents pushing the "Code block has no preceding
documentation" suggestion.

In `@packages/mcp/src/tools/reading.ts`:
- Line 6: The CSpell error arises because the token "modelcontextprotocol" used
in the import path (e.g., '@modelcontextprotocol/sdk/types.js') isn't in the
spell-check dictionary; fix it by adding "modelcontextprotocol" to your CSpell
configuration's custom words list (or equivalent dictionary entry) so the
spell-checker accepts imports like '@modelcontextprotocol/sdk/types.js' without
failing; update the cspell config (cspell.json / cspell.yaml or package.json
cspell section) to include "modelcontextprotocol" in the "words" array.
- Around line 183-187: Extract the duplicated helper loadDeepnoteFile (which
resolves a path, reads file content, and calls deserializeDeepnoteFile) into a
shared utility module (e.g., export an async function loadDeepnoteFile in a new
utils/file.ts or utils/deepnote.ts). Replace the duplicate implementations in
reading.ts, execution.ts, and writing.ts by importing loadDeepnoteFile from the
new module and remove the local functions; ensure you also export or reuse
deserializeDeepnoteFile from the same shared module or import it where needed so
callers still call deserializeDeepnoteFile via the shared helper.
- Around line 604-675: The isPythonBuiltin function recreates the Set on every
call; hoist the Set to module scope by defining a constant (e.g.,
PYTHON_BUILTINS or PYTHON_BUILTINS_SET) at top-level and initialize it once with
the listed names, then update isPythonBuiltin to simply return
PYTHON_BUILTINS.has(name); ensure the constant name is exported only if needed
and preserved exactly where the current array contents are defined.

In `@packages/mcp/src/tools/writing.ts`:
- Around line 928-937: The code constructs a RegExp from user-controlled
transform.replaceContent.find and passes it to new RegExp, which risks ReDoS; to
fix, treat find as a literal unless explicitly trusted: add an escapeRegExp
utility and use new RegExp(escapeRegExp(transform.replaceContent.find), 'g') (or
otherwise validate/sanitize the pattern when regex semantics are required), and
wrap the RegExp construction/replace call in a try/catch to safely handle
invalid or problematic patterns; update the block.content.replace usage to use
the escaped/validated pattern (and reference transform.replaceContent.find, new
RegExp, and block.content.replace to locate the change).

Comment thread packages/mcp/README.md
Comment thread packages/mcp/README.md
Comment thread packages/mcp/README.md
Comment thread packages/mcp/src/resources.ts
Comment thread packages/mcp/src/resources.ts
Comment thread packages/mcp/src/tools/magic.ts Outdated
Comment thread packages/mcp/src/tools/reading.ts
Comment thread packages/mcp/src/tools/reading.ts Outdated
Comment thread packages/mcp/src/tools/reading.ts Outdated
Comment thread packages/mcp/src/tools/writing.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Fix all issues with AI agents
In `@examples/mcp-demos/ab_test_evaluator.deepnote`:
- Around line 21-23: The notebook markdown cell content string describing the
tool is grammatically awkward; rewrite the sentence in that cell (the value of
the content field in the markdown cell) to be clear and idiomatic—e.g., "This
notebook is an A/B test statistical analysis tool that accepts experiment data
with control and treatment groups, calculates required sample sizes for desired
power, performs t-tests and chi-square tests, computes confidence intervals,
visualizes conversion rates with error bars, and gives recommendations based on
statistical significance." Update the content field accordingly to replace the
original sentence.

In `@examples/mcp-demos/api_analytics_client.deepnote`:
- Around line 89-101: The get method currently can silently return None if
retries is 0; add an input validation at the start of Client.get (the get(self,
endpoint, params=None, retries=3) method) to ensure retries is a positive
integer (e.g., raise ValueError if retries < 1) so callers don’t receive None
unexpectedly, or alternatively normalize retries with something like retries =
max(1, int(retries)) before entering the retry loop; keep the existing
retry/backoff logic and ensure the function never reaches the final `return
None` path.

In `@examples/mcp-demos/churn_prediction.deepnote`:
- Around line 76-77: The import "StandardScaler" is unused; either remove it
from the import line or apply it to preprocess features. In the file's imports
(the line importing from sklearn.preprocessing), delete "StandardScaler" if you
don't standardize inputs, or instantiate and use StandardScaler (fit/transform
on your feature matrix, e.g., in the data preparation pipeline before
train_test_split) referencing StandardScaler to avoid the unused-import warning.
- Around line 21-22: Update the markdown cell whose content starts "This
notebook Customer churn prediction model..." to a complete sentence by adding
the missing verb; for example change it to "This notebook implements a customer
churn prediction model that loads customer data, performs feature engineering on
usage metrics, subscription history, and engagement data, trains a
classification model with hyperparameter tuning, evaluates model performance
with confusion matrix and ROC curve, and outputs churn probability scores."
Locate the markdown entry by the keys type: markdown and the content string to
apply the edit.
- Around line 125-142: The notebook cell uses plt.figure(), plt.xlabel(),
plt.ylabel(), plt.title(), and plt.show() but never imports matplotlib.pyplot;
add the missing import statement (import matplotlib.pyplot as plt) at the top of
this cell (or the top of the notebook) so calls to plt in the plotting block
will work; locate the plotting block that computes cm = confusion_matrix(...)
and calls plt.figure() / plt.show() to insert the import.

In `@examples/mcp-demos/README.md`:
- Around line 20-56: The README shows deepnote_template, deepnote_scaffold,
deepnote_enhance, and deepnote_workflow as shell commands but they are MCP tool
names implemented in packages/mcp/src/tools/magic.ts and must be invoked through
the MCP server (deepnote-mcp) or an MCP client, not run directly. Update the
examples to (1) state these are MCP tool identifiers, (2) show the correct
invocation pattern via the MCP server/client (e.g., calling the tool name
through deepnote-mcp or an MCP client API with the tool name and args payload),
and (3) replace the bash command blocks with illustrative MCP client calls or a
JSON RPC payload example referencing the tool names deepnote_template,
deepnote_scaffold, deepnote_enhance, and deepnote_workflow so users know how to
call them correctly.

In `@packages/mcp/src/tools/magic.ts`:
- Around line 1836-1838: The code uses the deprecated pandas API
fillna(method='ffill').fillna(method='bfill') on df_transformed; replace that
call with the modern chained methods df_transformed.ffill().bfill() (preserving
the existing assignment to df_transformed and the missing_before
calculation/print) to avoid the deprecation warning—update the expression that
assigns df_transformed in the block containing missing_before and the print.
- Around line 478-492: Both loadDeepnoteFile and saveDeepnoteFile lack error
handling for file I/O and YAML serialization/deserialization; wrap the body of
each function (the path.resolve, fs.readFile/fs.writeFile and
deserializeDeepnoteFile/yamlStringify calls) in try/catch blocks, and on error
throw a new Error (or a custom Error subclass) that includes the operation, the
filePath, and the original error message/stack to preserve context (e.g.,
"loadDeepnoteFile: failed to read/parse <filePath>: <originalError>"). Ensure
you rethrow or include the original error as a cause where supported so callers
get both the high-level context and the underlying error details.
- Around line 496-501: The function handleScaffold uses unsafe casts
(description and outputPath) without runtime checks; add explicit runtime
validation at the start of handleScaffold to ensure description and outputPath
are non-empty strings (and optionally validate projectName and style types), and
throw a clear Error if validation fails so callers that bypass schema validation
fail fast; update references to description/outputPath/projectName/style within
handleScaffold to rely on the validated local variables.
- Around line 478-481: The loadDeepnoteFile function currently resolves
arbitrary filePath with path.resolve(filePath) which allows path traversal;
update loadDeepnoteFile (and any saveDeepnoteFile siblings) to validate the
resolved absolutePath is contained within the intended workspace root before
reading/writing: compute const absolutePath = path.resolve(filePath), compute
the workspaceRootResolved = path.resolve(workspaceRoot) (or use path.relative
like resources.ts) and if the absolutePath is outside workspaceRootResolved
(e.g. !absolutePath.startsWith(workspaceRootResolved) or
path.relative(workspaceRootResolved, absolutePath).startsWith('..')), throw a
clear error like 'Path traversal not allowed' and only proceed to
fs.readFile()/fs.writeFile() when the check passes.

Comment thread examples/mcp-demos/ab_test_evaluator.deepnote
Comment thread examples/mcp-demos/api_analytics_client.deepnote
Comment thread examples/mcp-demos/churn_prediction.deepnote
Comment thread examples/mcp-demos/churn_prediction.deepnote
Comment thread examples/mcp-demos/churn_prediction.deepnote
Comment thread examples/mcp-demos/README.md
Comment thread packages/mcp/src/tools/magic.ts Outdated
Comment thread packages/mcp/src/tools/magic.ts Outdated
Comment thread packages/mcp/src/tools/magic.ts Outdated
Comment thread packages/mcp/src/tools/magic.ts Outdated
Adds a simple CLI utility to test MCP tools directly without
needing a full MCP client connection. Usage:
  pnpm --filter @deepnote/mcp test:tool <tool-name> '<json-args>'

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@docs-dictionary.txt`:
- Around line 296-297: Two truncated dictionary entries "explor" and "analys"
should be replaced with their full intended words; update the entries in
docs-dictionary.txt by replacing "explor" with the appropriate full form (e.g.,
"explore" or "exploration") and "analys" with the appropriate full form (e.g.,
"analyse" or "analysis") so the dictionary contains complete tokens and
consistent spelling variants.

In `@packages/mcp/package.json`:
- Line 52: The dependency entry for "zod" in package.json is pinned to an exact
version ("3.25.76") while other deps use caret ranges; update the "zod"
dependency to use a caret range (change "zod": "3.25.76" to "zod": "^3.25.76")
unless the exact pin was intentional—if you intend to move to v4, instead set it
to a compatible careted v4 version (e.g., "^4.x") and update any usages
accordingly.

In `@packages/mcp/src/test-tool.ts`:
- Around line 79-81: The code computes scriptDir with
path.dirname(process.argv[1] || ''), which yields '.' when process.argv[1] is
undefined and can produce an incorrect path for resolving bin.js; change the
fallback to a reliable file path (e.g., use process.argv[1] ?? __filename or
process.argv[1] || __filename) so that scriptDir = path.dirname(process.argv[1]
?? __filename) (or similar) inside the Promise block where scriptDir is
declared, ensuring you reference process.argv[1] and __filename (or
process.cwd() if you prefer) to avoid an empty-string dirname.
- Around line 53-56: Remove the redundant null/undefined check for
toolArgsString: delete the if block that checks if (!toolArgsString) and its
console.error/process.exit calls in test-tool.ts because args[1] is guaranteed
when the earlier args.length >= 2 check passes; rely on the existing args length
validation (variable args) and the toolArgsString usage instead of keeping this
extra guard.
- Around line 123-126: The code calls process.exit(1) inside the promise when
response.error is truthy, which leaves the promise unresolved; instead reject
the promise with the error so callers can handle it. Replace the process.exit(1)
path in the block that checks response.error (the branch referencing
response.error.message || response.error) to call the surrounding Promise's
reject(...) with response.error (or new Error(String(response.error))) and
return afterward (do not call resolve), ensuring the promise is settled instead
of forcing exit.

Comment thread docs-dictionary.txt
Comment thread packages/mcp/package.json
Comment thread packages/mcp/src/test-tool.ts
Comment thread packages/mcp/src/test-tool.ts
Comment thread packages/mcp/src/test-tool.ts
- prompts.test.ts: 10 tests for MCP prompts and getPrompt function
- resources.test.ts: 10 tests for resource discovery and reading
- tools/tools.test.ts: 22 tests for tool definitions and schemas
- tools/magic.test.ts: 16 tests for magic tool handlers

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@packages/mcp/src/prompts.test.ts`:
- Around line 56-72: Add tests in prompts.test.ts that call
getPrompt('convert_and_enhance') and getPrompt('fix_and_document') without
passing the required param objects to cover default-argument behavior; ensure
each call does not throw, that result.messages[0].content exists and its text
contains the same tokens you already assert for the parameterized cases (e.g.,
for convert_and_enhance assert presence of 'deepnote_convert_to',
'deepnote_enhance' and a placeholder or default filename if applicable; for
fix_and_document assert 'deepnote_lint' and 'deepnote_fix'), so the contract is
locked when args are omitted.

In `@packages/mcp/src/resources.test.ts`:
- Around line 116-146: Add a test case that verifies readResource handles file
paths containing spaces and unicode by creating a temp file whose name includes
spaces and a non-ASCII character (e.g., "indiv ü test.deepnote"), writing the
same notebook content to that path (use the existing test's testNotebook and
filePath creation logic but with special chars), call readResource with an
appropriately encoded URI (use encodeURI or encodeURIComponent on the path when
building `deepnote://file/${filePath}`), and assert the same expectations as the
existing "reads individual notebook file" test (contents length, uri equals the
encoded URI, mimeType, and parsed data contains projectName or error). Reference
the existing test block and the readResource function to locate where to
add/modify the test.
- Around line 10-16: The afterEach cleanup assumes tempDir was created and calls
fs.rm unconditionally; if fs.mkdtemp in beforeEach throws this will attempt to
delete an undefined path and mask the original error. Modify the test setup so
tempDir is declared/initialized (e.g., let tempDir: string | undefined) and
change the afterEach to only call await fs.rm(tempDir, { recursive: true, force:
true }) when tempDir is defined/truthy; this ensures beforeEach's mkdtemp errors
are not shadowed and cleanup is skipped safely.

In `@packages/mcp/src/tools/magic.test.ts`:
- Around line 16-22: The afterEach cleanup currently calls fs.rm(tempDir...)
unconditionally which can throw or mask errors if mkdtemp failed; update
afterEach to guard removal by checking that tempDir is set and the directory
exists (e.g., typeof tempDir !== 'undefined' or a truthy check and optionally
fs.stat/fs.access) before calling fs.rm, or wrap the removal in a try/catch to
swallow only ENOENT-type errors so the original mkdtemp error is not masked;
reference the beforeEach (mkdtemp) and afterEach (fs.rm) and the tempDir
variable when making this change.

Comment thread packages/mcp/src/prompts.test.ts
Comment thread packages/mcp/src/resources.test.ts
Comment thread packages/mcp/src/resources.test.ts
Comment thread packages/mcp/src/tools/magic.test.ts Outdated
- Add 4 snapshot tools: list, load, split, merge
- Fix deepnote_run to run ALL notebooks by default (project level)
- Document 3 execution levels: project, notebook, block
- Document that snapshots are runnable for debugging

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@packages/mcp/src/tools/snapshots.ts`:
- Around line 158-162: The error responses in handleSnapshotList (the
early-return when filePath is falsy and the similar block around lines 195-198)
are missing the isError: true flag; update those returned objects to include
isError: true so they match the pattern used in handleSnapshotTool (see its
default/error branch in handleSnapshotTool) and ensure all error return shapes
consistently include isError: true alongside the content payload.
- Around line 219-227: The code repeats the same logic to count notebook blocks
with outputs (iterating snapshot.project.notebooks -> notebook.blocks and
checking (block as { outputs?: unknown[] }).outputs) in four places; extract
that into a single helper named countBlocksWithOutputs(snapshot:
DeepnoteSnapshot): number that iterates snapshot.project.notebooks and
increments when (block as { outputs?: unknown[] }).outputs?.length is truthy,
return the count, and replace each duplicated loop (the totalOutputs variable
code at the shown diff and the similar blocks at the other locations) with a
call to countBlocksWithOutputs(snapshot) to compute the value. Ensure the helper
is exported or scoped where all call sites can access it and keep the same
typing for DeepnoteSnapshot and the execBlock cast.
- Around line 205-209: Several early-return error responses in snapshots.ts
return a content payload with an error message but omit the standardized isError
flag; update every error return (e.g., the filePath guard and the other error
blocks mentioned around lines 305-309, 317-321, 394-399, 408-412, 480-485) so
the response includes isError: true alongside the existing content structure
(for example JSON.stringify({ error: '...', isError: true })). Ensure every
branch that returns an error object uses the same shape: { isError: true,
content: [...] } or embeds isError: true inside the JSON error payload where
currently missing.

Comment thread packages/mcp/src/tools/snapshots.ts
Comment thread packages/mcp/src/tools/snapshots.ts
Comment thread packages/mcp/src/tools/snapshots.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/mcp/src/tools/tools.test.ts`:
- Around line 85-100: The test currently checks that several tools require a
'path' parameter but misses asserting that deepnote_validate requires 'path' and
that deepnote_diff requires 'path1' and 'path2'; update the test loop to include
'deepnote_validate' in the pathRequiredTools list (so readingTools.find(...) and
schema checks validate presence of schema.properties.path and schema.required
contains 'path' for deepnote_validate), and add a separate assertion block for
the 'deepnote_diff' tool (use readingTools.find(t => t.name === 'deepnote_diff')
to get its InputSchema and assert schema.properties.path1 and
schema.properties.path2 are defined and schema.required contains both 'path1'
and 'path2').

Comment thread packages/mcp/src/tools/tools.test.ts
- Add vi.unstubAllGlobals() to afterEach in run.test.ts to prevent test leaks
- Add debug logging for temp directory cleanup failures in run.ts
- Add error handling for parseQuartoFormat, parseMarimoFormat, and
  parsePercentFormat in format-converter.ts with user-friendly error messages
Add test that verifies openDeepnoteInCloud is called when converting
to .deepnote format with --open flag.
jamesbhobbs and others added 10 commits January 29, 2026 10:25
- Save/restore extglob state when using extended glob patterns in
  bash completion for run and convert commands
- Add extglob and shopt to cspell dictionary
- Rename openDeepnoteInCloud to openDeepnoteFileInCloud for clarity
- Rename open-in-cloud.ts to open-file-in-cloud.ts
Resolved conflicts in --context flag for run command.
After every deepnote run, outputs are now automatically saved to a
snapshot file at snapshots/{slug}_{projectId}_latest.snapshot.deepnote.

This makes execution outputs persistent and available for later
inspection, which is useful for LLM use cases where outputs need
to be analyzed from previous runs.

- Add output-persistence.ts with mergeOutputsIntoFile and saveExecutionSnapshot
- Update run.ts to save snapshot after every execution
- Snapshot saving is best-effort (errors logged but dont fail run)
…ave and multi-format support

- Update server instructions to clearly explain that all execution outputs
  (stdout, stderr, charts, errors, timing) are saved to snapshot files
- Add automatic snapshot saving after deepnote_run execution
- Add multi-format support: deepnote_run now accepts .ipynb, .py, .qmd files
- Add debug_execution prompt showing how to use snapshots for debugging
- Update tool descriptions to reference snapshotPath in responses
- Restore example snapshots for documentation purposes
- Fix tests to work with updated examples structure

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Fix all issues with AI agents
In `@packages/blocks/src/deserialize-file/validate-all-deepnote-files.test.ts`:
- Around line 87-103: findSnapshots duplicates the recursive traversal logic in
findDeepnoteFiles; extract a single helper (e.g., findFilesRecursive or
walkAndFilter) that accepts (dir: string, predicate: (entry: Dirent, fullPath:
string) => boolean) and reuses readdir with withFileTypes, recursion, and error
handling, then replace both findSnapshots and findDeepnoteFiles to call this
helper with appropriate predicates (one checking
entry.name.endsWith('.snapshot.deepnote') for findSnapshots). Ensure the new
helper returns the same shape (Array<{ fileName: string; fullPath: string }>)
and update any tests that reference either function accordingly.
- Around line 108-114: The test currently returns early when
snapshotFiles.length === 0 which makes it silently pass; change it to
conditionally skip so the test output shows the skip and/or log a warning.
Replace the early-return pattern in the 'discovers .snapshot.deepnote files in
test-fixtures' test by selecting the test runner based on snapshotFiles (e.g.,
const maybeIt = snapshotFiles.length === 0 ? it.skip : it) and use maybeIt(...)
for the test, and add a console.warn or similar message when
snapshotFiles.length === 0 so the CI/test output clearly indicates why the test
was skipped; reference the snapshotFiles variable and the test name when making
the change.
- Around line 99-101: The empty catch in the test silently swallows all
filesystem errors; change it to catch the error object (catch (err)) and only
suppress the expected "directory not found" case (err.code === 'ENOENT' or
similar), but rethrow or log and fail for other errors (e.g., permission errors
like EACCES) so unexpected FS issues surface; update the catch block in
validate-all-deepnote-files.test.ts accordingly to either rethrow non-ENOENT
errors or call console.error / fail the test.

In `@packages/cli/src/commands/diff.test.ts`:
- Around line 359-361: Replace the manual fixture path construction in the diff
test (the file1 and file2 variables that call resolve(process.cwd(),
'test-fixtures/diff/...')) with the standard helper getDiffFixturePath() used by
other tests; specifically call
getDiffFixturePath('base-modified-blocks.deepnote') for the base fixture and
getDiffFixturePath('modified-blocks.deepnote') for the modified fixture so the
test uses the same fixture-loading pattern as the rest of the suite.

In `@packages/mcp/src/instructions.ts`:
- Line 122: The instruction text "Use deepnote_lint to catch issues before
running" is missing inline code formatting; update that literal so it matches
the style used elsewhere (e.g., change the phrase to use backticks around
deepnote_lint) — look for the exact string "Use deepnote_lint to catch issues
before running" in packages/mcp/src/instructions.ts and wrap deepnote_lint in
backticks to be consistent with the `deepnote_lint` usage on line 87.

In `@packages/mcp/src/prompts.ts`:
- Around line 67-84: Ensure required prompt arguments are actually enforced:
update getPrompt to validate that required args are present before delegating to
helpers (getDebugExecutionPrompt, getCreateNotebookPrompt,
getConvertAndEnhancePrompt, getFixAndDocumentPrompt); for each case, check the
specific required keys declared in that prompt's schema (or maintain a small map
of required keys per prompt name) and throw a clear Error when a required arg is
missing, or alternatively remove the `required: true` flags from the prompt
schemas if you prefer keeping defaults—make the change so the declared schema
accurately reflects runtime behavior.

In `@packages/mcp/src/tools/execution.ts`:
- Around line 559-561: In deepnote_open, guard the fs.readFile(absolutePath)
call so a file removed between stat and read doesn't throw: wrap the read in a
try/catch (or a Promise .catch) around the fs.readFile call that produces
fileBuffer and, on error, return the proper error to the caller/handler (e.g.,
call the open callback with an ENOENT/EIO appropriate errno) and log the failure
instead of letting the exception escape; update any code paths that use
fileBuffer to only proceed after successful read.
- Around line 449-456: The call to loadDeepnoteFile inside handleRunBlock can
throw on missing/invalid files and currently lets the exception escape the tool;
wrap the await loadDeepnoteFile(filePath) in a try/catch inside handleRunBlock,
catch errors from loadDeepnoteFile and return or throw a handled tool response
(e.g., a structured error/result for the deepnote_run_block tool) that includes
a clear message and the original error message, so the tool handler doesn't
crash; update any callers or the deepnote_run_block contract to expect this
handled error shape if needed.
- Around line 457-460: The variables targetBlock and targetNotebook are declared
as "let ... = null" which yields implicit any; change their declarations to
explicit nullable types (e.g., the concrete Block and Notebook types used in
this module) — replace the implicit any with the proper type unions (Block |
null and Notebook | null or the exact names used elsewhere in this file) and add
any needed imports for those types so strictTypeChecking passes; update the
declarations for targetBlock and targetNotebook in execution.ts accordingly.
- Around line 290-299: The call to resolveAndConvertToDeepnote inside handleRun
can throw before the existing try/catch; wrap that call in its own try/catch so
conversion/parse errors are caught and converted into the tool's structured
failure response (e.g., return a consistent object with a success/error/message
shape) instead of bubbling out. Specifically, update handleRun to catch
exceptions from resolveAndConvertToDeepnote, extract the error message/stack,
and return the same structured error format used by other failures in this
handler; reference the handleRun function and the resolveAndConvertToDeepnote
call when making the change.

Comment thread packages/blocks/src/deserialize-file/validate-all-deepnote-files.test.ts Outdated
Comment thread packages/blocks/src/deserialize-file/validate-all-deepnote-files.test.ts Outdated
Comment thread packages/blocks/src/deserialize-file/validate-all-deepnote-files.test.ts Outdated
Comment thread packages/cli/src/commands/diff.test.ts Outdated
Comment thread packages/mcp/src/instructions.ts Outdated
Comment thread packages/mcp/src/prompts.ts
Comment thread packages/mcp/src/tools/execution.ts
Comment thread packages/mcp/src/tools/execution.ts Outdated
Comment thread packages/mcp/src/tools/execution.ts
Comment thread packages/mcp/src/tools/execution.ts Outdated
- Replace __dirname with import.meta.url for ESM compatibility in resources.ts
- Fix examples resource listing which was crashing with "__dirname is not defined"
- Add try-catch for JSON.parse in workflow results to handle non-JSON outputs
- Fix workflow tool crashing when using deepnote_explain (which returns markdown)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@packages/mcp/src/tools/magic.ts`:
- Around line 2489-2523: The switch on step.tool in this routing block only maps
scaffold/enhance/fix/explain/suggest/template/refactor/profile/test to handler
functions (handleScaffold, handleEnhance, handleFix, etc.), but the
schema/description advertises extra tools like convert_to and lint; update the
routing to match the schema by adding cases for the missing tools (e.g., case
'convert_to': result = await handleConvertTo(toolArgs); case 'lint': result =
await handleLint(toolArgs);), ensuring corresponding handler functions
(handleConvertTo, handleLint) exist and follow the same result shape, or
alternatively remove/adjust convert_to and lint from the schema/description so
they are not advertised; keep the default Unknown tool branch as a fallback.
- Around line 1454-1459: The visualization-detection predicate incorrectly
scopes only plt. to code blocks and treats sns. in any block as a visualization;
update the predicate used in the notebook.blocks.some(...) check so both checks
are constrained to code blocks (i.e., change the arrow function for
notebook.blocks.some to require b.type === 'code' and then test
b.content?.includes('plt.') || b.content?.includes('sns.')). This touches the
block that checks block.content for '.describe()' or '.value_counts()' and the
notebook.blocks.some(...) predicate in packages/mcp/src/tools/magic.ts.

Comment thread packages/mcp/src/tools/magic.ts Outdated
Comment thread packages/mcp/src/tools/magic.ts Outdated
jamesbhobbs and others added 5 commits January 29, 2026 12:44
- Log directory scan errors to stderr for debugging
- Log snapshot save failures to stderr for debugging
…ar deps, and refactor

enhance tool:
- Now actually applies input blocks (input-slider) for hardcoded values like
  test_size, n_estimators, threshold, learning_rate, max_depth
- Modifies code to reference the input variable instead of hardcoded value
- Adds section headers (h1, h2) when structure enhancement is applied

fix tool:
- Implements circular dependency detection using DAG analysis
- Attempts to break cycles by reordering blocks (moving high-output blocks earlier)
- Reports detected cycles with the variables involved

refactor tool:
- Now removes extracted functions/classes from source blocks
- Adds import statements to source notebooks (from <module> import <names>)
- Reports sourceModified and importAdded in response

Updated tool descriptions to accurately reflect new capabilities.
…ar deps, and refactor (#258)

enhance tool:
- Now actually applies input blocks (input-slider) for hardcoded values like
  test_size, n_estimators, threshold, learning_rate, max_depth
- Modifies code to reference the input variable instead of hardcoded value
- Adds section headers (h1, h2) when structure enhancement is applied

fix tool:
- Implements circular dependency detection using DAG analysis
- Attempts to break cycles by reordering blocks (moving high-output blocks earlier)
- Reports detected cycles with the variables involved

refactor tool:
- Now removes extracted functions/classes from source blocks
- Adds import statements to source notebooks (from <module> import <names>)
- Reports sourceModified and importAdded in response

Updated tool descriptions to accurately reflect new capabilities.
- Add Structure section explaining block → notebook → project hierarchy
- Add execution scope guidance: deepnote_run_block → run notebook → run project
- Add incremental development workflow for complex/custom logic
- Update Quick Start to recommend incremental approach
- Add execution scoping tips to Best Practices

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/mcp/src/tools/execution.ts`:
- Around line 300-373: The notebook filter accepts name or ID but you pass the
raw notebookFilter into runProject and the dry-run metadata, causing failures
when an ID is used; resolve the notebook by finding it (the existing const
found) and pass found.name into ExecutionEngine.runProject's notebookName option
and use found.name for the dry-run "level: 'notebook'" metadata and notebooks
list instead of notebookFilter; update references around notebookFilter, found,
runProject, and the dry-run JSON creation so all notebook names are the resolved
names.

Comment thread packages/mcp/src/tools/execution.ts
Base automatically changed from cli-comb2 to main January 30, 2026 08:49
Reduce tool calls and token usage by:
- Add includeOutputSummary to deepnote_run (default: true) to inline truncated
  block outputs, eliminating need for snapshot_load in most cases
- Add unified deepnote_read tool that combines inspect/stats/lint/dag operations
  with configurable include parameter
- Add compact output mode to key tools for minimal JSON formatting
- Add workflow presets (quickstart, import, polish) to deepnote_workflow for
  common multi-step operations in a single call
- Shorten verbose tool descriptions to reduce system prompt tokens

Changes:
- packages/mcp/src/tools/execution.ts: Add includeOutputSummary, compact params
- packages/mcp/src/tools/reading.ts: Add deepnote_read, formatOutput helper
- packages/mcp/src/tools/magic.ts: Add presets to workflow, compact to tools
- packages/mcp/src/server.ts: Route deepnote_read to reading handler
- Add reading.test.ts and update tools.test.ts, magic.test.ts with new tests

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@packages/mcp/src/tools/magic.test.ts`:
- Around line 232-261: Test assertion is too weak and can pass when nothing
changed; update the deepnote_enhance test (describe 'deepnote_enhance' in
magic.test.ts) to assert that blocks were actually added and/or contain
documentation: after calling handleMagicTool('deepnote_enhance', ...), use
extractResult as done, then assert
expect(afterBlockCount).toBeGreaterThan(beforeBlockCount) (strict increase) and
additionally inspect afterDeepnote.project.notebooks[0].blocks for a newly added
block (compare block ids or check for a block whose content/metadata indicates
documentation, e.g., text blocks containing "Documentation" or similar) to
ensure the enhancement produced documentation blocks rather than relying only on
count.
- Around line 9-11: The extractResult function assumes response.content[0].text
exists and is valid JSON; add defensive checks and clearer errors: in
extractResult ensure response and response.content are defined and that
response.content.length>0 and response.content[0].text is a non-empty string,
then wrap JSON.parse(response.content[0].text) in a try/catch and rethrow a new
Error that includes contextual details (e.g., "extractResult: invalid JSON in
content[0].text" plus the original parse error and the raw text) so tests fail
with a clear message instead of cryptic exceptions.

In `@packages/mcp/src/tools/reading.test.ts`:
- Around line 8-11: The extractResult function is duplicated; refactor by moving
the implementation into a shared test utility module (e.g., export function
extractResult(response: { content: Array<{ type: string; text: string }> }):
Record<string, unknown> from a new shared tests/utils file) and update both
reading.test.ts and magic.test.ts to import that exported extractResult instead
of declaring it locally; ensure the function signature/type is exported so both
tests compile.
- Around line 14-30: The afterEach cleanup unconditionally calls fs.rm on
tempDir which can be undefined if mkdtemp or handleMagicTool failed; update the
afterEach to guard deletion by checking that tempDir is truthy (and optionally
exists) before calling fs.rm, referencing the tempDir variable and the
beforeEach setup that uses fs.mkdtemp and handleMagicTool to ensure you only
attempt cleanup when tempDir was successfully created.

In `@packages/mcp/src/tools/reading.ts`:
- Around line 377-384: The catch currently swallows errors; change the bare
catch to catch (err) and include the error details when reporting so debugging
is possible: update the catch in packages/mcp/src/tools/reading.ts to capture
the thrown error (err), then either call the available logger (e.g.,
processLogger.error or console.error) with a message like "Error analyzing
dependencies" and the notebook name, and/or augment the issues.push entry (which
currently uses severity:'warning' and message:'Could not analyze dependencies')
to include err.message or err.stack (e.g., add an error or details field) so the
notebook name and the actual error are recorded for troubleshooting.

Comment thread packages/mcp/src/tools/magic.test.ts Outdated
Comment thread packages/mcp/src/tools/magic.test.ts Outdated
Comment thread packages/mcp/src/tools/reading.test.ts
Comment thread packages/mcp/src/tools/reading.test.ts
Comment thread packages/mcp/src/tools/reading.ts Outdated
jamesbhobbs and others added 3 commits February 2, 2026 09:43
Resolved conflicts in:
- packages/cli/src/cli.ts (analyze command output format)
- packages/cli/src/commands/analyze.ts (dependencies output formatting)
- packages/cli/src/commands/run.ts (imports, BlockWithContext, O(n) lookups)
- packages/cli/src/commands/stats.ts (use analyzeProject for imports)
- packages/cli/src/utils/analysis.ts (DAG-based import extraction, env vars)
- packages/cli/src/utils/analysis.test.ts (env var isolation, import tests)
- packages/cli/src/utils/output-persistence.ts (executionCount handling)
- packages/cli/src/utils/output-persistence.test.ts (use fixture loader)

Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor: cleanup but keep largely same

* refactor: cull tools to make llm model do the looping

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In `@packages/mcp/src/tools/conversion.ts`:
- Around line 92-103: The auto-detect steps (fs.readFile and detectFormat)
execute before the surrounding try/catch, so errors from readFile or
detectFormat bypass the structured handler; move the auto-detection logic into
the existing try block (or wrap it in its own try/catch) so that failures are
caught and routed to the same error handling path, ensuring format,
finalOutputPath, and finalProjectName are computed inside that guarded scope and
that detectFormat(absoluteInput, content) and fs.readFile(...) errors are
handled consistently.

In `@packages/mcp/src/tools/execution.test.ts`:
- Around line 8-10: The helper function extractResult is duplicated in
execution.test.ts and reading.test.ts; create a single shared utility file
(e.g., test-utils.ts) that exports extractResult, move the implementation there,
then replace the local definitions in both execution.test.ts and reading.test.ts
with imports from the new test-utils module and remove the duplicates so both
tests import and call the shared extractResult.
- Around line 13-34: The afterEach cleanup currently calls fs.rm(tempDir...)
unconditionally which will throw if tempDir was never set due to a setup
failure; update the afterEach to guard against undefined/empty tempDir (e.g., if
(tempDir) await fs.rm(tempDir, { recursive: true, force: true })) so cleanup
only runs when beforeEach succeeded and tempDir was assigned; keep the existing
tempDir/testNotebookPath setup in beforeEach and the
handleWritingTool('deepnote_create', ...) call unchanged.

In `@packages/mcp/src/tools/snapshots.test.ts`:
- Around line 16-87: Add a test that creates a notebook whose name contains
special characters (e.g., spaces, punctuation, unicode) via
handleWritingTool('deepnote_create') using the same tempDir/testNotebookPath
setup, then call handleSnapshotTool('deepnote_snapshot_split') (and optionally
'deepnote_snapshot_list') against that notebook and assert the produced
latestPath/snapshotPath contains a normalized slug (no unsafe chars) and that
the snapshot file exists; target the existing tests under
deepnote_snapshot_split, referencing the existing helpers handleWritingTool,
handleSnapshotTool, testNotebookPath, tempDir, and assert on
result.latestPath/result.snapshotPath and filesystem existence to cover the
special-character filename edge case.

In `@packages/mcp/src/tools/writing.ts`:
- Around line 534-536: The template literal in the changes.push call can produce
"undefined..." when targetBlock.content is undefined; instead compute a safe
preview string before building the template (e.g., oldPreview =
targetBlock.content ? targetBlock.content.slice(0,50) + "..." : "<empty>") and
use that in the changes.push (`changes.push(\`content: "${oldPreview}" →
"${newContent.slice(0,50)}..."\`)`), ensuring you still check newContent !==
undefined and avoid calling slice on an undefined value.
- Around line 10-12: The current findBlock implementation can return the wrong
block when multiple DeepnoteBlock ids share a prefix; change findBlock to first
attempt an exact match on id (compare b.id === idOrPrefix) and only if none
found perform prefix matching but validate that the prefix match yields exactly
one candidate (using blocks.filter with b.id.startsWith(idOrPrefix)); if
multiple matches exist, return undefined or throw a clear error so callers know
the lookup is ambiguous. Ensure you update any callers of findBlock to handle
the undefined/error case appropriately.

In `@packages/mcp/src/utils.ts`:
- Around line 26-38: The function formatOutput should accept data:
Record<string, unknown> instead of data: object to avoid implicit any from
Object.entries; update the function signature and adjust the filtering callback
in formatOutput to guard against null (e.g., check v === null) before using
typeof v === 'object' and when checking Object.keys(v).length ensure v is cast
or narrowed to Record<string, unknown> (or checked with typeof and non-null) so
TypeScript knows it’s an object; keep the rest of the compact filtering logic
and return values the same.

Comment on lines +92 to +103
// Auto-detect format if needed
if (format === 'auto') {
const content = await fs.readFile(absoluteInput, 'utf-8')
format = detectFormat(absoluteInput, content)
}

// Determine output path
const finalOutputPath = outputPath || absoluteInput.replace(/\.(ipynb|qmd|py)$/, '.deepnote')

// Determine project name
const finalProjectName = projectName || path.basename(absoluteInput).replace(/\.(ipynb|qmd|py)$/, '')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Auto‑detect errors bypass the error handler.
readFile/detectFormat run before the try, so failures throw and skip the structured error response.

🐛 Suggested fix
-  // Auto-detect format if needed
-  if (format === 'auto') {
-    const content = await fs.readFile(absoluteInput, 'utf-8')
-    format = detectFormat(absoluteInput, content)
-  }
-
-  // Determine output path
-  const finalOutputPath = outputPath || absoluteInput.replace(/\.(ipynb|qmd|py)$/, '.deepnote')
-
-  // Determine project name
-  const finalProjectName = projectName || path.basename(absoluteInput).replace(/\.(ipynb|qmd|py)$/, '')
-
-  try {
+  try {
+    // Auto-detect format if needed
+    if (format === 'auto') {
+      const content = await fs.readFile(absoluteInput, 'utf-8')
+      format = detectFormat(absoluteInput, content)
+    }
+
+    // Determine output path
+    const finalOutputPath = outputPath || absoluteInput.replace(/\.(ipynb|qmd|py)$/, '.deepnote')
+
+    // Determine project name
+    const finalProjectName = projectName || path.basename(absoluteInput).replace(/\.(ipynb|qmd|py)$/, '')
🤖 Prompt for AI Agents
In `@packages/mcp/src/tools/conversion.ts` around lines 92 - 103, The auto-detect
steps (fs.readFile and detectFormat) execute before the surrounding try/catch,
so errors from readFile or detectFormat bypass the structured handler; move the
auto-detection logic into the existing try block (or wrap it in its own
try/catch) so that failures are caught and routed to the same error handling
path, ensuring format, finalOutputPath, and finalProjectName are computed inside
that guarded scope and that detectFormat(absoluteInput, content) and
fs.readFile(...) errors are handled consistently.

Comment on lines +8 to +10
function extractResult(response: { content: Array<{ type: string; text: string }> }): Record<string, unknown> {
return JSON.parse(response.content[0].text)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

extractResult duplicated across test files.

Same helper exists in reading.test.ts. Extract to shared utility.

Example shared utility
// packages/mcp/src/tools/test-utils.ts
export function extractResult(response: { content: Array<{ type: string; text: string }> }): Record<string, unknown> {
  return JSON.parse(response.content[0].text)
}
🤖 Prompt for AI Agents
In `@packages/mcp/src/tools/execution.test.ts` around lines 8 - 10, The helper
function extractResult is duplicated in execution.test.ts and reading.test.ts;
create a single shared utility file (e.g., test-utils.ts) that exports
extractResult, move the implementation there, then replace the local definitions
in both execution.test.ts and reading.test.ts with imports from the new
test-utils module and remove the duplicates so both tests import and call the
shared extractResult.

Comment on lines +13 to +34
let tempDir: string
let testNotebookPath: string

beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-execution-test-'))

testNotebookPath = path.join(tempDir, 'test.deepnote')
await handleWritingTool('deepnote_create', {
outputPath: testNotebookPath,
projectName: 'Test Project',
notebooks: [
{
name: 'Notebook',
blocks: [{ type: 'code', content: 'print("hello world")' }],
},
],
})
})

afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true })
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard tempDir cleanup against setup failures.

Same pattern issue as reading.test.ts.

Proposed fix
-  let tempDir: string
+  let tempDir: string | undefined
   let testNotebookPath: string

   beforeEach(async () => {
     tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-execution-test-'))
     // ...
   })

   afterEach(async () => {
-    await fs.rm(tempDir, { recursive: true, force: true })
+    if (tempDir) {
+      await fs.rm(tempDir, { recursive: true, force: true })
+    }
   })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let tempDir: string
let testNotebookPath: string
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-execution-test-'))
testNotebookPath = path.join(tempDir, 'test.deepnote')
await handleWritingTool('deepnote_create', {
outputPath: testNotebookPath,
projectName: 'Test Project',
notebooks: [
{
name: 'Notebook',
blocks: [{ type: 'code', content: 'print("hello world")' }],
},
],
})
})
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true })
})
let tempDir: string | undefined
let testNotebookPath: string
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-execution-test-'))
testNotebookPath = path.join(tempDir, 'test.deepnote')
await handleWritingTool('deepnote_create', {
outputPath: testNotebookPath,
projectName: 'Test Project',
notebooks: [
{
name: 'Notebook',
blocks: [{ type: 'code', content: 'print("hello world")' }],
},
],
})
})
afterEach(async () => {
if (tempDir) {
await fs.rm(tempDir, { recursive: true, force: true })
}
})
🤖 Prompt for AI Agents
In `@packages/mcp/src/tools/execution.test.ts` around lines 13 - 34, The afterEach
cleanup currently calls fs.rm(tempDir...) unconditionally which will throw if
tempDir was never set due to a setup failure; update the afterEach to guard
against undefined/empty tempDir (e.g., if (tempDir) await fs.rm(tempDir, {
recursive: true, force: true })) so cleanup only runs when beforeEach succeeded
and tempDir was assigned; keep the existing tempDir/testNotebookPath setup in
beforeEach and the handleWritingTool('deepnote_create', ...) call unchanged.

Comment on lines +16 to +87
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-snapshot-test-'))

testNotebookPath = path.join(tempDir, 'test.deepnote')
await handleWritingTool('deepnote_create', {
outputPath: testNotebookPath,
projectName: 'Test Project',
notebooks: [
{
name: 'Notebook',
blocks: [{ type: 'code', content: 'print("hello")' }],
},
],
})
})

afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true })
})

describe('deepnote_snapshot_list', () => {
it('handles missing snapshot directory gracefully', async () => {
const response = await handleSnapshotTool('deepnote_snapshot_list', {
path: testNotebookPath,
})

const result = extractResult(response)
// When no snapshots directory exists, returns empty list
expect(result.snapshotsFound).toBe(0)
expect(result.snapshots).toEqual([])
})

it('returns error when path is missing', async () => {
const response = await handleSnapshotTool('deepnote_snapshot_list', {})
const result = extractResult(response)
expect(result.error).toBe('path is required')
})
})

describe('deepnote_snapshot_split', () => {
it('splits source from outputs', async () => {
const response = await handleSnapshotTool('deepnote_snapshot_split', {
path: testNotebookPath,
snapshotDir: path.join(tempDir, 'snapshots'),
})

const result = extractResult(response)
expect(result.success).toBe(true)
expect(result.sourcePath).toBeDefined()
expect(result.snapshotPath).toBeDefined()
expect(typeof result.outputsExtracted).toBe('number')

// Snapshot file should exist
const snapshotPath = result.snapshotPath as string
const stat = await fs.stat(snapshotPath)
expect(stat.isFile()).toBe(true)
})

it('creates latest snapshot by default', async () => {
const response = await handleSnapshotTool('deepnote_snapshot_split', {
path: testNotebookPath,
snapshotDir: path.join(tempDir, 'snapshots'),
})

const result = extractResult(response)
expect(result.latestPath).toBeDefined()
const latestPath = result.latestPath as string
expect(latestPath).toContain('_latest.snapshot.deepnote')

const stat = await fs.stat(latestPath)
expect(stat.isFile()).toBe(true)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add a special‑character case for snapshot filenames.
Guidelines require special‑character coverage; the slug normalization in snapshot filenames isn’t exercised.

✅ Suggested test addition
+  it('handles special characters in project names when generating snapshot filenames', async () => {
+    const specialPath = path.join(tempDir, 'special.deepnote')
+    await handleWritingTool('deepnote_create', {
+      outputPath: specialPath,
+      projectName: 'Tęšt / Project: 1',
+      notebooks: [{ name: 'Notebook', blocks: [{ type: 'code', content: 'print("x")' }] }],
+    })
+
+    const response = await handleSnapshotTool('deepnote_snapshot_split', {
+      path: specialPath,
+      snapshotDir: path.join(tempDir, 'snapshots'),
+    })
+
+    const result = extractResult(response)
+    expect(result.snapshotPath).toContain('test-project-1')
+  })

As per coding guidelines, Test edge cases, error handling, and special characters in Vitest tests.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-snapshot-test-'))
testNotebookPath = path.join(tempDir, 'test.deepnote')
await handleWritingTool('deepnote_create', {
outputPath: testNotebookPath,
projectName: 'Test Project',
notebooks: [
{
name: 'Notebook',
blocks: [{ type: 'code', content: 'print("hello")' }],
},
],
})
})
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true })
})
describe('deepnote_snapshot_list', () => {
it('handles missing snapshot directory gracefully', async () => {
const response = await handleSnapshotTool('deepnote_snapshot_list', {
path: testNotebookPath,
})
const result = extractResult(response)
// When no snapshots directory exists, returns empty list
expect(result.snapshotsFound).toBe(0)
expect(result.snapshots).toEqual([])
})
it('returns error when path is missing', async () => {
const response = await handleSnapshotTool('deepnote_snapshot_list', {})
const result = extractResult(response)
expect(result.error).toBe('path is required')
})
})
describe('deepnote_snapshot_split', () => {
it('splits source from outputs', async () => {
const response = await handleSnapshotTool('deepnote_snapshot_split', {
path: testNotebookPath,
snapshotDir: path.join(tempDir, 'snapshots'),
})
const result = extractResult(response)
expect(result.success).toBe(true)
expect(result.sourcePath).toBeDefined()
expect(result.snapshotPath).toBeDefined()
expect(typeof result.outputsExtracted).toBe('number')
// Snapshot file should exist
const snapshotPath = result.snapshotPath as string
const stat = await fs.stat(snapshotPath)
expect(stat.isFile()).toBe(true)
})
it('creates latest snapshot by default', async () => {
const response = await handleSnapshotTool('deepnote_snapshot_split', {
path: testNotebookPath,
snapshotDir: path.join(tempDir, 'snapshots'),
})
const result = extractResult(response)
expect(result.latestPath).toBeDefined()
const latestPath = result.latestPath as string
expect(latestPath).toContain('_latest.snapshot.deepnote')
const stat = await fs.stat(latestPath)
expect(stat.isFile()).toBe(true)
})
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-snapshot-test-'))
testNotebookPath = path.join(tempDir, 'test.deepnote')
await handleWritingTool('deepnote_create', {
outputPath: testNotebookPath,
projectName: 'Test Project',
notebooks: [
{
name: 'Notebook',
blocks: [{ type: 'code', content: 'print("hello")' }],
},
],
})
})
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true })
})
describe('deepnote_snapshot_list', () => {
it('handles missing snapshot directory gracefully', async () => {
const response = await handleSnapshotTool('deepnote_snapshot_list', {
path: testNotebookPath,
})
const result = extractResult(response)
// When no snapshots directory exists, returns empty list
expect(result.snapshotsFound).toBe(0)
expect(result.snapshots).toEqual([])
})
it('returns error when path is missing', async () => {
const response = await handleSnapshotTool('deepnote_snapshot_list', {})
const result = extractResult(response)
expect(result.error).toBe('path is required')
})
})
describe('deepnote_snapshot_split', () => {
it('splits source from outputs', async () => {
const response = await handleSnapshotTool('deepnote_snapshot_split', {
path: testNotebookPath,
snapshotDir: path.join(tempDir, 'snapshots'),
})
const result = extractResult(response)
expect(result.success).toBe(true)
expect(result.sourcePath).toBeDefined()
expect(result.snapshotPath).toBeDefined()
expect(typeof result.outputsExtracted).toBe('number')
// Snapshot file should exist
const snapshotPath = result.snapshotPath as string
const stat = await fs.stat(snapshotPath)
expect(stat.isFile()).toBe(true)
})
it('creates latest snapshot by default', async () => {
const response = await handleSnapshotTool('deepnote_snapshot_split', {
path: testNotebookPath,
snapshotDir: path.join(tempDir, 'snapshots'),
})
const result = extractResult(response)
expect(result.latestPath).toBeDefined()
const latestPath = result.latestPath as string
expect(latestPath).toContain('_latest.snapshot.deepnote')
const stat = await fs.stat(latestPath)
expect(stat.isFile()).toBe(true)
})
it('handles special characters in project names when generating snapshot filenames', async () => {
const specialPath = path.join(tempDir, 'special.deepnote')
await handleWritingTool('deepnote_create', {
outputPath: specialPath,
projectName: 'Tęšt / Project: 1',
notebooks: [{ name: 'Notebook', blocks: [{ type: 'code', content: 'print("x")' }] }],
})
const response = await handleSnapshotTool('deepnote_snapshot_split', {
path: specialPath,
snapshotDir: path.join(tempDir, 'snapshots'),
})
const result = extractResult(response)
expect(result.snapshotPath).toContain('test-project-1')
})
})
🤖 Prompt for AI Agents
In `@packages/mcp/src/tools/snapshots.test.ts` around lines 16 - 87, Add a test
that creates a notebook whose name contains special characters (e.g., spaces,
punctuation, unicode) via handleWritingTool('deepnote_create') using the same
tempDir/testNotebookPath setup, then call
handleSnapshotTool('deepnote_snapshot_split') (and optionally
'deepnote_snapshot_list') against that notebook and assert the produced
latestPath/snapshotPath contains a normalized slug (no unsafe chars) and that
the snapshot file exists; target the existing tests under
deepnote_snapshot_split, referencing the existing helpers handleWritingTool,
handleSnapshotTool, testNotebookPath, tempDir, and assert on
result.latestPath/result.snapshotPath and filesystem existence to cover the
special-character filename edge case.

Comment on lines +10 to +12
function findBlock(blocks: DeepnoteBlock[], idOrPrefix: string): DeepnoteBlock | undefined {
return blocks.find(b => b.id === idOrPrefix || b.id.startsWith(idOrPrefix))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Prefix matching may cause ambiguous block lookups.

b.id.startsWith(idOrPrefix) could match multiple blocks if IDs share prefixes. Consider exact match first or validating uniqueness.

Safer approach
 function findBlock(blocks: DeepnoteBlock[], idOrPrefix: string): DeepnoteBlock | undefined {
-  return blocks.find(b => b.id === idOrPrefix || b.id.startsWith(idOrPrefix))
+  // Prefer exact match
+  const exact = blocks.find(b => b.id === idOrPrefix)
+  if (exact) return exact
+  // Fall back to prefix match only if unambiguous
+  const prefixMatches = blocks.filter(b => b.id.startsWith(idOrPrefix))
+  return prefixMatches.length === 1 ? prefixMatches[0] : undefined
 }
🤖 Prompt for AI Agents
In `@packages/mcp/src/tools/writing.ts` around lines 10 - 12, The current
findBlock implementation can return the wrong block when multiple DeepnoteBlock
ids share a prefix; change findBlock to first attempt an exact match on id
(compare b.id === idOrPrefix) and only if none found perform prefix matching but
validate that the prefix match yields exactly one candidate (using blocks.filter
with b.id.startsWith(idOrPrefix)); if multiple matches exist, return undefined
or throw a clear error so callers know the lookup is ambiguous. Ensure you
update any callers of findBlock to handle the undefined/error case
appropriately.

Comment on lines +534 to +536
if (newContent !== undefined) {
changes.push(`content: "${targetBlock.content?.slice(0, 50)}..." → "${newContent.slice(0, 50)}..."`)
targetBlock.content = newContent

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential crash on undefined content.

targetBlock.content?.slice(0, 50) is guarded, but the template literal still runs. If content is undefined, output shows "undefined...".

Proposed fix
   if (newContent !== undefined) {
-    changes.push(`content: "${targetBlock.content?.slice(0, 50)}..." → "${newContent.slice(0, 50)}..."`)
+    const oldPreview = targetBlock.content ? `"${targetBlock.content.slice(0, 50)}..."` : '(empty)'
+    changes.push(`content: ${oldPreview} → "${newContent.slice(0, 50)}..."`)
     targetBlock.content = newContent
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (newContent !== undefined) {
changes.push(`content: "${targetBlock.content?.slice(0, 50)}..." → "${newContent.slice(0, 50)}..."`)
targetBlock.content = newContent
if (newContent !== undefined) {
const oldPreview = targetBlock.content ? `"${targetBlock.content.slice(0, 50)}..."` : '(empty)'
changes.push(`content: ${oldPreview} → "${newContent.slice(0, 50)}..."`)
targetBlock.content = newContent
🤖 Prompt for AI Agents
In `@packages/mcp/src/tools/writing.ts` around lines 534 - 536, The template
literal in the changes.push call can produce "undefined..." when
targetBlock.content is undefined; instead compute a safe preview string before
building the template (e.g., oldPreview = targetBlock.content ?
targetBlock.content.slice(0,50) + "..." : "<empty>") and use that in the
changes.push (`changes.push(\`content: "${oldPreview}" →
"${newContent.slice(0,50)}..."\`)`), ensuring you still check newContent !==
undefined and avoid calling slice on an undefined value.

Comment thread packages/mcp/src/utils.ts
Comment on lines +26 to +38
export function formatOutput(data: object, compact: boolean): string {
if (compact) {
const filtered = Object.fromEntries(
Object.entries(data).filter(([_, v]) => {
if (v == null) return false
if (Array.isArray(v) && v.length === 0) return false
if (typeof v === 'object' && !Array.isArray(v) && Object.keys(v).length === 0) return false
return true
})
)
return JSON.stringify(filtered)
}
return JSON.stringify(data, null, 2)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if file exists and show the function
fd -t f "utils.ts" packages/mcp/src/

Repository: deepnote/deepnote

Length of output: 86


🏁 Script executed:

# Show the actual content of the file to verify
cat -n packages/mcp/src/utils.ts | head -50

Repository: deepnote/deepnote

Length of output: 2083


🏁 Script executed:

# Check biome.json for relevant linting rules
find . -name "biome.json" -type f | head -1 | xargs cat

Repository: deepnote/deepnote

Length of output: 4178


🌐 Web query:

TypeScript Object.entries return type object parameter type any implicit

💡 Result:

Object.entries() often “returns any” in TypeScript because its standard typings fall back to a broad overload when the input is typed as object / {} (or otherwise not a keyed record). In that case, the result is effectively [string, any][], so destructuring in a callback can trigger “binding element/parameter implicitly has an any type” under strict / noImplicitAny. [1][3]

Fix 1: Don’t type the input as object; use a keyed type

const obj: Record<string, number> = { a: 1, b: 2 };
const e = Object.entries(obj); // [string, number][]

Fix 2: Use a typed helper to preserve keyof/value types

type Entries<T> = { [K in keyof T]-?: [K, T[K]] }[keyof T];

function typedEntries<T extends Record<PropertyKey, unknown>>(o: T) {
  return Object.entries(o) as Array<Entries<T>>;
}

// usage
const obj = { a: 1, b: 2 } as const;
for (const [k, v] of typedEntries(obj)) {
  // k: "a" | "b"
  // v: 1 | 2
}

This pattern is commonly used because the built-in Object.entries() typing is intentionally conservative for non-exact object types. [3]

Fix 3 (if you’re getting “Property ‘entries’ does not exist…”): enable the right lib

Add an ES2017+ lib (or specifically es2017.object) in tsconfig.json:

{
  "compilerOptions": {
    "lib": ["es2017", "dom"]
  }
}

(or any later es20xx). [2]

Sources: [1] [2] [3]


🏁 Script executed:

# Check tsconfig.json for strict mode settings
fd -t f "tsconfig.json" | head -3 | xargs cat

Repository: deepnote/deepnote

Length of output: 793


Use Record<string, unknown> to avoid implicit any in Object.entries.

With data: object, Object.entries returns [string, any][], violating strict type checking. Switch to Record<string, unknown> and add a null guard for empty object checks.

Suggested refactor
-export function formatOutput(data: object, compact: boolean): string {
+export function formatOutput(data: Record<string, unknown>, compact: boolean): string {
   if (compact) {
     const filtered = Object.fromEntries(
-      Object.entries(data).filter(([_, v]) => {
+      Object.entries(data).filter(([, v]) => {
         if (v == null) return false
         if (Array.isArray(v) && v.length === 0) return false
-        if (typeof v === 'object' && !Array.isArray(v) && Object.keys(v).length === 0) return false
+        if (typeof v === 'object' && v && !Array.isArray(v) && Object.keys(v as Record<string, unknown>).length === 0) return false
         return true
       })
     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function formatOutput(data: object, compact: boolean): string {
if (compact) {
const filtered = Object.fromEntries(
Object.entries(data).filter(([_, v]) => {
if (v == null) return false
if (Array.isArray(v) && v.length === 0) return false
if (typeof v === 'object' && !Array.isArray(v) && Object.keys(v).length === 0) return false
return true
})
)
return JSON.stringify(filtered)
}
return JSON.stringify(data, null, 2)
export function formatOutput(data: Record<string, unknown>, compact: boolean): string {
if (compact) {
const filtered = Object.fromEntries(
Object.entries(data).filter(([, v]) => {
if (v == null) return false
if (Array.isArray(v) && v.length === 0) return false
if (typeof v === 'object' && v && !Array.isArray(v) && Object.keys(v as Record<string, unknown>).length === 0) return false
return true
})
)
return JSON.stringify(filtered)
}
return JSON.stringify(data, null, 2)
}
🤖 Prompt for AI Agents
In `@packages/mcp/src/utils.ts` around lines 26 - 38, The function formatOutput
should accept data: Record<string, unknown> instead of data: object to avoid
implicit any from Object.entries; update the function signature and adjust the
filtering callback in formatOutput to guard against null (e.g., check v ===
null) before using typeof v === 'object' and when checking Object.keys(v).length
ensure v is cast or narrowed to Record<string, unknown> (or checked with typeof
and non-null) so TypeScript knows it’s an object; keep the rest of the compact
filtering logic and return values the same.

@dinohamzic

dinohamzic commented Feb 10, 2026 •

Copy link
Copy Markdown
Contributor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants