-
Notifications
You must be signed in to change notification settings - Fork 171
feat(@deepnote/mcp): optimize tool invocations and token usage #259
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: mcp-skills
Are you sure you want to change the base?
Conversation
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
📝 WalkthroughWalkthroughThe PR adds a new unified 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/mcp/src/tools/magic.ts (2)
145-175:deepnote_explainmissingcompactschema property.Other tools have
compactin their input schema, butexplaindoesn't. The handler also doesn't use it. Inconsistent API surface.
363-411: Schema allows calling with no preset or steps.The
inputSchemahas norequiredarray. User could invokedeepnote_workflowwith empty args. Lines 2750-2755 handle this, but schema validation should catch it earlier.Schema improvement
}, }, + required: [], + oneOf: [ + { required: ['preset'] }, + { required: ['steps'] } + ], },Or add a note that one of
presetorstepsis required.
🤖 Fix all issues with AI agents
In `@packages/mcp/src/tools/execution.ts`:
- Around line 38-51: formatOutput currently only removes empty/null values at
the top level, allowing nested empty objects/arrays to remain; update
formatOutput to perform a deep/recursive filter when compact is true by walking
objects and arrays and removing nulls, empty arrays, and empty objects at any
depth (suggest adding a helper like deepFilterEmpty(value): any that returns
undefined for pruned values and recurses into arrays/objects), then
JSON.stringify the result of that helper when compact is true; ensure the helper
preserves non-empty primitives and avoids infinite recursion on circular refs
(e.g., by skipping or throwing if cycles detected).
In `@packages/mcp/src/tools/magic.ts`:
- Around line 2877-2886: The compact mapping in responseData currently always
sets error: r.error which yields explicit undefined entries; update the mapping
used in the compact branch (the results.map(...) expression) to only include the
error property when r.error is present (e.g., build each object with { step:
r.step, tool: r.tool, success: r.success, ...(r.error ? { error: r.error } : {})
}) so successful steps omit the error key before passing to formatOutput.
- Around line 2813-2820: Replace the dynamic import + .then pattern for the
reading and conversion tools with static top-level imports and direct calls: add
top-level imports for handleReadingTool and handleConversionTool, then in the
switch cases replace the await import(...).then(...) expressions with direct
calls like await handleReadingTool('deepnote_lint', toolArgs) and await
handleConversionTool('deepnote_convert_to', toolArgs) to restore TypeScript type
safety for those functions.
In `@packages/mcp/src/tools/reading.test.ts`:
- Around line 71-80: Add a new test that exercises the notebook filter support
in deepnote_read: call handleReadingTool('deepnote_read', { path:
testNotebookPath, include: ['dag'], notebook: '<notebookName>' }) (use the
scaffolded notebook name, e.g., 'Main'), then call extractResult(response) and
assert result.dag is defined, is an array, and filters to the expected single
entry (expect(dag.length).toBe(1)) and optionally assert that dag[0].notebook
=== '<notebookName>'; place this alongside the existing 'includes dag when
requested' test to cover the notebook filter edge case.
In `@packages/mcp/src/tools/reading.ts`:
- Around line 244-261: Duplicate implementation of formatOutput exists; extract
it into a single shared utility and import it where needed. Create a new module
(e.g., tools/utils.ts) that exports function formatOutput(data: object, compact:
boolean): string with the compact-mode filtering logic (ensure the object-empty
check excludes arrays by using !Array.isArray(v)). Then remove the local
formatOutput implementations and import the shared formatOutput in both places
that currently define it (the files that previously had the duplicate, including
the one named execution.ts), updating any references to call the imported
function.
| 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) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Shallow filter for nested structures.
formatOutput only filters top-level empty values. Nested empty arrays/objects pass through. Acceptable if intentional, but worth noting.
🤖 Prompt for AI Agents
In `@packages/mcp/src/tools/execution.ts` around lines 38 - 51, formatOutput
currently only removes empty/null values at the top level, allowing nested empty
objects/arrays to remain; update formatOutput to perform a deep/recursive filter
when compact is true by walking objects and arrays and removing nulls, empty
arrays, and empty objects at any depth (suggest adding a helper like
deepFilterEmpty(value): any that returns undefined for pruned values and
recurses into arrays/objects), then JSON.stringify the result of that helper
when compact is true; ensure the helper preserves non-empty primitives and
avoids infinite recursion on circular refs (e.g., by skipping or throwing if
cycles detected).
| case 'lint': | ||
| // Lint is a reading tool, import it | ||
| result = await import('./reading').then(m => m.handleReadingTool('deepnote_lint', toolArgs)) | ||
| break | ||
| case 'convert_to': | ||
| // Convert is a conversion tool, import it | ||
| result = await import('./conversion').then(m => m.handleConversionTool('deepnote_convert_to', toolArgs)) | ||
| break |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# Check if the exports exist in reading.ts and conversion.ts
rg -n "export.*handleReadingTool" packages/mcp/src/tools/
rg -n "export.*handleConversionTool" packages/mcp/src/tools/Repository: deepnote/deepnote
Length of output: 341
🏁 Script executed:
# Check the current imports at the top of magic.ts to see what's already imported
head -50 packages/mcp/src/tools/magic.ts | grep -E "^import|^from"Repository: deepnote/deepnote
Length of output: 494
🏁 Script executed:
# Get more context around the dynamic imports to understand the pattern
sed -n '2800,2830p' packages/mcp/src/tools/magic.tsRepository: deepnote/deepnote
Length of output: 1122
🏁 Script executed:
# Check if reading.ts or conversion.ts are already imported statically in magic.ts
rg -n "from.*['\"].*reading" packages/mcp/src/tools/magic.ts
rg -n "from.*['\"].*conversion" packages/mcp/src/tools/magic.tsRepository: deepnote/deepnote
Length of output: 43
🏁 Script executed:
# Check what these modules export and any potential circular dependencies
head -30 packages/mcp/src/tools/reading.ts | grep -E "^import|^export"
head -30 packages/mcp/src/tools/conversion.ts | grep -E "^import|^export"Repository: deepnote/deepnote
Length of output: 676
🏁 Script executed:
# Verify if reading.ts or conversion.ts import from magic.ts (potential circular deps)
rg -n "from.*magic" packages/mcp/src/tools/reading.ts
rg -n "from.*magic" packages/mcp/src/tools/conversion.tsRepository: deepnote/deepnote
Length of output: 43
Add type safety to dynamic imports of reading and conversion tools.
The .then() pattern provides no TypeScript type checking. Since no circular dependencies exist, use static imports at the file top:
import { handleReadingTool } from './reading'
import { handleConversionTool } from './conversion'Then call directly: result = await handleReadingTool('deepnote_lint', toolArgs).
🤖 Prompt for AI Agents
In `@packages/mcp/src/tools/magic.ts` around lines 2813 - 2820, Replace the
dynamic import + .then pattern for the reading and conversion tools with static
top-level imports and direct calls: add top-level imports for handleReadingTool
and handleConversionTool, then in the switch cases replace the await
import(...).then(...) expressions with direct calls like await
handleReadingTool('deepnote_lint', toolArgs) and await
handleConversionTool('deepnote_convert_to', toolArgs) to restore TypeScript type
safety for those functions.
| const responseData = { | ||
| success: allSuccess, | ||
| preset: preset || undefined, | ||
| stepsCompleted: results.length, | ||
| totalSteps: steps.length, | ||
| results: compact ? results.map(r => ({ step: r.step, tool: r.tool, success: r.success, error: r.error })) : results, | ||
| } | ||
|
|
||
| return { | ||
| content: [ | ||
| { | ||
| type: 'text', | ||
| text: JSON.stringify( | ||
| { | ||
| success: allSuccess, | ||
| stepsCompleted: results.length, | ||
| totalSteps: steps.length, | ||
| results, | ||
| }, | ||
| null, | ||
| 2 | ||
| ), | ||
| }, | ||
| ], | ||
| content: [{ type: 'text', text: formatOutput(responseData, compact || false) }], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Minor: error: r.error includes undefined values.
In compact mode, the mapped result objects include error: undefined for successful steps. This gets filtered by formatOutput, but explicit would be cleaner.
Cleaner mapping
- results: compact ? results.map(r => ({ step: r.step, tool: r.tool, success: r.success, error: r.error })) : results,
+ results: compact ? results.map(r => ({ step: r.step, tool: r.tool, success: r.success, ...(r.error && { error: r.error }) })) : results,🤖 Prompt for AI Agents
In `@packages/mcp/src/tools/magic.ts` around lines 2877 - 2886, The compact
mapping in responseData currently always sets error: r.error which yields
explicit undefined entries; update the mapping used in the compact branch (the
results.map(...) expression) to only include the error property when r.error is
present (e.g., build each object with { step: r.step, tool: r.tool, success:
r.success, ...(r.error ? { error: r.error } : {}) }) so successful steps omit
the error key before passing to formatOutput.
| it('includes dag when requested', async () => { | ||
| const response = await handleReadingTool('deepnote_read', { | ||
| path: testNotebookPath, | ||
| include: ['dag'], | ||
| }) | ||
|
|
||
| const result = extractResult(response) | ||
| expect(result.dag).toBeDefined() | ||
| expect(Array.isArray(result.dag)).toBe(true) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Consider adding notebook filter test.
deepnote_read supports a notebook param for DAG filtering. No test covers it.
💡 Example test
it('filters dag by notebook', async () => {
const response = await handleReadingTool('deepnote_read', {
path: testNotebookPath,
include: ['dag'],
notebook: 'Main', // or whatever the scaffolded notebook name is
})
const result = extractResult(response)
expect(result.dag).toBeDefined()
const dag = result.dag as Array<{ notebook: string }>
expect(dag.length).toBe(1)
})Based on learnings: "Test edge cases, error handling, and special characters in Vitest tests"
🤖 Prompt for AI Agents
In `@packages/mcp/src/tools/reading.test.ts` around lines 71 - 80, Add a new test
that exercises the notebook filter support in deepnote_read: call
handleReadingTool('deepnote_read', { path: testNotebookPath, include: ['dag'],
notebook: '<notebookName>' }) (use the scaffolded notebook name, e.g., 'Main'),
then call extractResult(response) and assert result.dag is defined, is an array,
and filters to the expected single entry (expect(dag.length).toBe(1)) and
optionally assert that dag[0].notebook === '<notebookName>'; place this
alongside the existing 'includes dag when requested' test to cover the notebook
filter edge case.
| /** | ||
| * Format output based on compact mode | ||
| */ | ||
| function formatOutput(data: object, compact: boolean): string { | ||
| if (compact) { | ||
| // Filter out null, undefined, and empty arrays/objects | ||
| 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' && Object.keys(v).length === 0) return false | ||
| return true | ||
| }) | ||
| ) | ||
| return JSON.stringify(filtered) | ||
| } | ||
| return JSON.stringify(data, null, 2) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Duplicate formatOutput implementation.
Same function exists in execution.ts (lines 38-51). Extract to shared module.
♻️ Suggested approach
Create packages/mcp/src/tools/utils.ts:
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)
}Then import in both files.
🤖 Prompt for AI Agents
In `@packages/mcp/src/tools/reading.ts` around lines 244 - 261, Duplicate
implementation of formatOutput exists; extract it into a single shared utility
and import it where needed. Create a new module (e.g., tools/utils.ts) that
exports function formatOutput(data: object, compact: boolean): string with the
compact-mode filtering logic (ensure the object-empty check excludes arrays by
using !Array.isArray(v)). Then remove the local formatOutput implementations and
import the shared formatOutput in both places that currently define it (the
files that previously had the duplicate, including the one named execution.ts),
updating any references to call the imported function.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## mcp-skills #259 +/- ##
==============================================
+ Coverage 72.81% 74.83% +2.02%
==============================================
Files 96 96
Lines 6909 7110 +201
Branches 1977 1999 +22
==============================================
+ Hits 5031 5321 +290
+ Misses 1877 1788 -89
Partials 1 1 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Reduce tool calls and token usage by:
Changes:
Summary by CodeRabbit
Release Notes
✏️ Tip: You can customize this high-level summary in your review settings.