-
Notifications
You must be signed in to change notification settings - Fork 171
feat(@deepnote/mcp): implement adaptive compact mode with auto-escala… #260
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-optim
Are you sure you want to change the base?
Conversation
…tion Add server mode system that defaults to compact mode for reduced token usage: - Hide redundant tools (inspect, stats, lint, dag) in compact mode - All handlers default to compact=true output (minimal JSON) - Auto-escalation wrapper retries failed operations with verbose output - Add deepnote_mode tool to switch between compact/full modes - Add compact parameter support to all snapshot tools - Update server instructions with performance modes documentation
📝 WalkthroughWalkthroughThis PR introduces a two-tier mode system to the MCP server: a default compact mode for streamlined tool output, and a full mode for verbose responses. A new deepnote_mode tool enables mode switching. When executing tools, the server automatically escalates from compact to full mode if an error occurs, retrying the operation. Tool descriptions and formatting logic have been updated across execution, magic, reading, and snapshot tools to use compact as true by default. Workflow instructions and tests document the new behavior. Sequence Diagram(s)sequenceDiagram
participant Client
participant Server as Server<br/>(Escalation Handler)
participant Tool as Tool<br/>(Execution Layer)
Client->>Server: Call Tool (in compact mode)
Server->>Tool: Execute (compact: true)
Tool-->>Server: Error Response
alt Escalation Triggered
Server->>Server: Check: compact enabled?
Server->>Tool: Retry (compact: false)
Tool-->>Server: Success with Full Output
Server-->>Client: Response + Escalation Hint
else No Escalation
Server-->>Client: Error Response
end
🚥 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 |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## mcp-skills-optim #260 +/- ##
====================================================
- Coverage 74.83% 74.31% -0.53%
====================================================
Files 96 96
Lines 7110 7164 +54
Branches 1999 2017 +18
====================================================
+ Hits 5321 5324 +3
- Misses 1788 1839 +51
Partials 1 1 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
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 (1)
847-866: Compact default isn’t applied to verbose fields or workflow propagation.
compactnow defaults to true, but truthiness checks still treatundefinedas false, so hints/sections/results stay verbose by default and workflow drops explicitcompact: false. DeriveisCompact = compact !== falseand pass through explicit booleans.🛠️ Suggested fix
@@ async function handleScaffold(args: Record<string, unknown>) { - const compact = args.compact as boolean | undefined + const compact = args.compact as boolean | undefined + const isCompact = compact !== false @@ - sections: compact + sections: isCompact @@ - hint: compact + hint: isCompact @@ - text: formatOutput(responseData, compact !== false), + text: formatOutput(responseData, isCompact), @@ async function handleWorkflow(args: Record<string, unknown>) { - const compact = args.compact as boolean | undefined + const compact = args.compact as boolean | undefined + const isCompact = compact !== false @@ - ...(compact ? { compact } : {}), + ...(typeof compact === 'boolean' ? { compact } : {}), @@ - result: compact ? undefined : parsedResult, + result: isCompact ? undefined : parsedResult, @@ - results: compact ? results.map(r => ({ step: r.step, tool: r.tool, success: r.success, error: r.error })) : results, + results: isCompact ? results.map(r => ({ step: r.step, tool: r.tool, success: r.success, error: r.error })) : results, @@ - content: [{ type: 'text', text: formatOutput(responseData, compact !== false) }], + content: [{ type: 'text', text: formatOutput(responseData, isCompact) }],Also applies to: 2722-2750, 2840-2887
packages/mcp/src/tools/execution.ts (1)
475-505: Compact default isn’t applied to execution/results fields.With
compactdefaulting to true,executionand fullresultsstill show unless compact is explicitly true. Use a derivedisCompact = compact !== falseconsistently.🛠️ Suggested fix
@@ async function handleRun(args: Record<string, unknown>) { - const compact = args.compact as boolean | undefined + const compact = args.compact as boolean | undefined + const isCompact = compact !== false @@ - execution: compact + execution: isCompact ? undefined : { startedAt: executionStartedAt, finishedAt: executionFinishedAt, }, - results: compact ? results.filter(r => !r.success || r.error) : results, + results: isCompact ? results.filter(r => !r.success || r.error) : results, @@ - text: formatOutput(responseData, compact !== false), + text: formatOutput(responseData, isCompact),
🤖 Fix all issues with AI agents
In `@packages/mcp/src/instructions.ts`:
- Around line 157-175: The documentation still recommends deepnote_lint in the
Compact-mode best-practices; update the guidance to reference deepnote_read with
include=[lint] (or include=[structure,stats,lint,dag] as the combined example)
instead of deepnote_lint, and ensure the Compact-mode bullet explicitly states
that linting should be invoked via deepnote_read include=[lint] while
deepnote_mode mode=full remains the way to enable granular standalone tools.
In `@packages/mcp/src/server.test.ts`:
- Around line 6-43: Add Vitest tests that cover switching to the new deepnote
mode and the auto‑escalation/retry behavior: call resetServerMode() then set
server mode to 'deepnote' (or invoke the API/function that toggles mode), assert
getServerMode() returns 'deepnote', and verify readingTools (and any tool list
provider used in tests) includes the new deepnote-specific tool and excludes
compact-only hidden tools; additionally add a test that simulates a failed
read/operation and asserts the escalation/retry path occurs (e.g., triggers the
escalation code path and checks the output/tool selection after escalation).
Reference resetServerMode, getServerMode, and readingTools when adding these
tests and implement mocks/stubs as needed to simulate failures for the
retry/escalation assertions.
In `@packages/mcp/src/server.ts`:
- Around line 111-144: executeWithEscalation currently retries every failing
tool which can duplicate side effects for non-idempotent tools (writes,
execution, snapshot, conversion); change the retry gate to only allow escalation
when the tool is known idempotent or when the caller explicitly opts in. In
practice: inside executeWithEscalation, consult a property on the handler
invocation (e.g., safeArgs.allowEscalation === true) or a tool metadata flag
(e.g., tool.family or tool.isIdempotent) before performing the retry; if the
tool family is one of writing/execution/snapshot/conversion (or isIdempotent is
false/undefined) do not call handler again and return the original result.
Ensure the new gate is checked both for the initial call and for any other
similar retry logic (e.g., other occurrences around lines 203-260) so
non-idempotent tools never get auto-retried unless explicitly opted-in.
- Around line 87-105: Validate args.mode before changing the global serverMode
inside handleModeSwitch: check that args.mode is one of the allowed ServerMode
values (e.g., using a type guard, enum check, or allowed-values set) and if it
is invalid return an error response (success: false and a descriptive message)
without mutating serverMode or calling getFilteredTools; only assign serverMode
= newMode and proceed to build the success payload when the validation passes.
In `@packages/mcp/src/tools/snapshots.ts`:
- Around line 15-31: The parameter type for formatOutput should be changed from
object to Record<string, unknown> to avoid implicit any from Object.entries;
update the function signature of formatOutput and any related callers to accept
Record<string, unknown>, and when iterating Object.entries(data) treat each
value as unknown and narrow it (e.g., use Array.isArray, v == null, typeof v ===
'object' checks) before accessing properties or Object.keys; specifically adjust
the filtering callback around Object.entries(data) and any type assertions so
the code compiles under noImplicitAny and Biome rules.
| ## Performance Modes | ||
|
|
||
| The server operates in two modes for optimal performance: | ||
|
|
||
| **Compact mode (default):** Optimized for speed and token efficiency | ||
| - Responses are minimal single-line JSON | ||
| - Redundant tools hidden (use \`deepnote_read\` instead of inspect/stats/lint/dag) | ||
| - Use \`deepnote_workflow\` with presets for multi-step operations | ||
| - If an error occurs, the server automatically retries with verbose output | ||
|
|
||
| **Full mode:** For debugging or when you need more detail | ||
| - Call \`deepnote_mode mode=full\` to switch | ||
| - All granular tools available (inspect, stats, lint, dag) | ||
| - Verbose output with hints and suggestions | ||
|
|
||
| **When you see "Escalated to verbose mode":** The operation encountered an issue and was retried with full output for debugging. After resolving, continue using compact mode. | ||
|
|
||
| **Recommended:** Use \`deepnote_read\` with \`include=[structure,stats,lint,dag]\` to combine multiple analysis operations in one call. | ||
|
|
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.
Best‑practices bullet still points to deepnote_lint.
Compact mode hides lint, so keep guidance aligned with deepnote_read include=[lint] (or full mode).
📝 Suggested tweak
- - Use `deepnote_lint` to catch issues before running
+ - Use `deepnote_read include=[lint]` (or `deepnote_lint` in full mode) to catch issues before running🤖 Prompt for AI Agents
In `@packages/mcp/src/instructions.ts` around lines 157 - 175, The documentation
still recommends deepnote_lint in the Compact-mode best-practices; update the
guidance to reference deepnote_read with include=[lint] (or
include=[structure,stats,lint,dag] as the combined example) instead of
deepnote_lint, and ensure the Compact-mode bullet explicitly states that linting
should be invoked via deepnote_read include=[lint] while deepnote_mode mode=full
remains the way to enable granular standalone tools.
| describe('server mode and tool filtering', () => { | ||
| beforeEach(() => { | ||
| resetServerMode() | ||
| }) | ||
|
|
||
| afterEach(() => { | ||
| resetServerMode() | ||
| }) | ||
|
|
||
| describe('server mode state', () => { | ||
| it('defaults to compact mode', () => { | ||
| expect(getServerMode()).toBe('compact') | ||
| }) | ||
|
|
||
| it('can be reset to compact', () => { | ||
| // This test verifies resetServerMode works | ||
| resetServerMode() | ||
| expect(getServerMode()).toBe('compact') | ||
| }) | ||
| }) | ||
|
|
||
| describe('compact mode hidden tools', () => { | ||
| const COMPACT_HIDDEN_TOOLS = ['deepnote_inspect', 'deepnote_stats', 'deepnote_lint', 'deepnote_dag'] | ||
|
|
||
| it('reading tools include the tools that should be hidden in compact mode', () => { | ||
| const names = readingTools.map(t => t.name) | ||
| // Verify these tools exist in reading tools (they get filtered by server in compact mode) | ||
| for (const hiddenTool of COMPACT_HIDDEN_TOOLS) { | ||
| expect(names).toContain(hiddenTool) | ||
| } | ||
| }) | ||
|
|
||
| it('reading tools include deepnote_read as replacement', () => { | ||
| const names = readingTools.map(t => t.name) | ||
| expect(names).toContain('deepnote_read') | ||
| }) | ||
| }) | ||
| }) |
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.
Missing tests for deepnote_mode switching and auto‑escalation.
Current coverage verifies defaults and filtering but not the new mode tool or retry/escalation behavior. Add tests to flip modes and validate tool lists/output after escalation. As per coding guidelines Create comprehensive tests for all new features using Vitest as the testing framework.
🤖 Prompt for AI Agents
In `@packages/mcp/src/server.test.ts` around lines 6 - 43, Add Vitest tests that
cover switching to the new deepnote mode and the auto‑escalation/retry behavior:
call resetServerMode() then set server mode to 'deepnote' (or invoke the
API/function that toggles mode), assert getServerMode() returns 'deepnote', and
verify readingTools (and any tool list provider used in tests) includes the new
deepnote-specific tool and excludes compact-only hidden tools; additionally add
a test that simulates a failed read/operation and asserts the escalation/retry
path occurs (e.g., triggers the escalation code path and checks the output/tool
selection after escalation). Reference resetServerMode, getServerMode, and
readingTools when adding these tests and implement mocks/stubs as needed to
simulate failures for the retry/escalation assertions.
| function handleModeSwitch(args: Record<string, unknown>) { | ||
| const newMode = args.mode as ServerMode | ||
| const oldMode = serverMode | ||
| serverMode = newMode | ||
|
|
||
| return { | ||
| content: [ | ||
| { | ||
| type: 'text', | ||
| text: JSON.stringify({ | ||
| success: true, | ||
| previousMode: oldMode, | ||
| currentMode: newMode, | ||
| toolsAvailable: getFilteredTools().length, | ||
| hint: | ||
| newMode === 'full' | ||
| ? 'Full mode enabled. Switch back to compact mode for faster responses.' | ||
| : 'Compact mode enabled. Use deepnote_read for inspect/stats/lint/dag.', | ||
| }), |
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.
Validate mode input before mutating global state.
args.mode is blindly cast; invalid values will leave the server in an undefined mode and bypass compact filtering. Guard and return an error.
Suggested fix
function handleModeSwitch(args: Record<string, unknown>) {
- const newMode = args.mode as ServerMode
+ const newMode = args.mode
+ if (newMode !== 'compact' && newMode !== 'full') {
+ return {
+ content: [{ type: 'text', text: JSON.stringify({ error: 'mode must be "compact" or "full"' }) }],
+ isError: true,
+ }
+ }
const oldMode = serverMode
serverMode = newMode🤖 Prompt for AI Agents
In `@packages/mcp/src/server.ts` around lines 87 - 105, Validate args.mode before
changing the global serverMode inside handleModeSwitch: check that args.mode is
one of the allowed ServerMode values (e.g., using a type guard, enum check, or
allowed-values set) and if it is invalid return an error response (success:
false and a descriptive message) without mutating serverMode or calling
getFilteredTools; only assign serverMode = newMode and proceed to build the
success payload when the validation passes.
| /** | ||
| * Execute a tool with auto-escalation: if compact mode fails, retry with verbose output | ||
| */ | ||
| async function executeWithEscalation( | ||
| handler: ( | ||
| name: string, | ||
| args: Record<string, unknown> | undefined | ||
| ) => Promise<{ | ||
| content: Array<{ type: string; text?: string }> | ||
| isError?: boolean | ||
| }>, | ||
| name: string, | ||
| args: Record<string, unknown> | undefined | ||
| ) { | ||
| const safeArgs = args || {} | ||
|
|
||
| // First attempt - uses default compact=true (since we changed defaults) | ||
| const result = await handler(name, safeArgs) | ||
|
|
||
| // If it failed and compact wasn't explicitly disabled, retry with verbose output | ||
| if (result.isError && safeArgs.compact !== false) { | ||
| const fullResult = await handler(name, { ...safeArgs, compact: false }) | ||
|
|
||
| // Add de-escalation hint to the response | ||
| fullResult.content.push({ | ||
| type: 'text', | ||
| text: '\n---\n[Escalated to verbose mode for debugging. Use compact=true or omit for faster responses.]', | ||
| }) | ||
|
|
||
| return fullResult | ||
| } | ||
|
|
||
| return result | ||
| } |
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.
Auto-retry can duplicate side effects on non-idempotent tools.
All tool families are retried on error; writing/execution/snapshot/conversion can mutate workspace or trigger runs. Gate retries to idempotent/read-only tools or require explicit opt-in.
Suggested fix
-async function executeWithEscalation(
+async function executeWithEscalation(
handler: (
name: string,
args: Record<string, unknown> | undefined
) => Promise<{
content: Array<{ type: string; text?: string }>
isError?: boolean
}>,
name: string,
- args: Record<string, unknown> | undefined
+ args: Record<string, unknown> | undefined,
+ retryOnError = true
) {
const safeArgs = args || {}
// First attempt - uses default compact=true (since we changed defaults)
const result = await handler(name, safeArgs)
// If it failed and compact wasn't explicitly disabled, retry with verbose output
- if (result.isError && safeArgs.compact !== false) {
+ if (retryOnError && result.isError && safeArgs.compact !== false) {
const fullResult = await handler(name, { ...safeArgs, compact: false })
@@
- return await executeWithEscalation(handleWritingTool, name, args)
+ return await executeWithEscalation(handleWritingTool, name, args, false)
@@
- return await executeWithEscalation(handleConversionTool, name, args)
+ return await executeWithEscalation(handleConversionTool, name, args, false)
@@
- return await executeWithEscalation(handleExecutionTool, name, args)
+ return await executeWithEscalation(handleExecutionTool, name, args, false)
@@
- return await executeWithEscalation(handleSnapshotTool, name, args)
+ return await executeWithEscalation(handleSnapshotTool, name, args, false)Also applies to: 203-260
🤖 Prompt for AI Agents
In `@packages/mcp/src/server.ts` around lines 111 - 144, executeWithEscalation
currently retries every failing tool which can duplicate side effects for
non-idempotent tools (writes, execution, snapshot, conversion); change the retry
gate to only allow escalation when the tool is known idempotent or when the
caller explicitly opts in. In practice: inside executeWithEscalation, consult a
property on the handler invocation (e.g., safeArgs.allowEscalation === true) or
a tool metadata flag (e.g., tool.family or tool.isIdempotent) before performing
the retry; if the tool family is one of writing/execution/snapshot/conversion
(or isIdempotent is false/undefined) do not call handler again and return the
original result. Ensure the new gate is checked both for the initial call and
for any other similar retry logic (e.g., other occurrences around lines 203-260)
so non-idempotent tools never get auto-retried unless explicitly opted-in.
| /** | ||
| * Format output based on compact mode - omit null/empty, use single-line JSON | ||
| */ | ||
| 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.
🧩 Analysis chain
🏁 Script executed:
# First, let's examine the full context of the file to understand how formatOutput is used
cat -n packages/mcp/src/tools/snapshots.ts | head -50Repository: deepnote/deepnote
Length of output: 2034
🏁 Script executed:
# Check if formatOutput is called elsewhere and what data types are passed
rg 'formatOutput' packages/mcp/src/tools/snapshots.ts -B 2 -A 2Repository: deepnote/deepnote
Length of output: 862
🏁 Script executed:
# Let's check the biome configuration to understand linting rules
cat biome.json | head -100Repository: deepnote/deepnote
Length of output: 3258
🏁 Script executed:
# Check if there are TypeScript config settings relevant to typing strictness
if [ -f tsconfig.json ]; then cat tsconfig.json | head -50; fiRepository: deepnote/deepnote
Length of output: 793
🏁 Script executed:
# Verify TypeScript's Object.entries typing behavior by checking what the actual types resolve to
cat > /tmp/test-entries.ts << 'EOF'
// Test how Object.entries types with different parameter types
function testObject(data: object) {
const entries = Object.entries(data);
// entries should be [string, any][]
entries.forEach(([k, v]) => {
// v should be any here
});
}
function testRecord(data: Record<string, unknown>) {
const entries = Object.entries(data);
// entries should be [string, unknown][]
entries.forEach(([k, v]) => {
// v should be unknown here
});
}
EOF
cat /tmp/test-entries.tsRepository: deepnote/deepnote
Length of output: 516
🏁 Script executed:
# Check the actual usage context - what types are passed to formatOutput
rg 'responseData' packages/mcp/src/tools/snapshots.ts -B 3 -A 1 | head -40Repository: deepnote/deepnote
Length of output: 832
Use Record<string, unknown> instead of object to avoid implicit any.
Object.entries(data) with data: object resolves to [string, any][], violating project's noImplicitAny and Biome's noExplicitAny rules. Since formatOutput processes external/runtime data from tools, Record<string, unknown> maintains type safety.
Suggested change
-function formatOutput(data: object, compact: boolean): string {
+function formatOutput(data: Record<string, unknown>, compact: boolean): string {📝 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.
| /** | |
| * Format output based on compact mode - omit null/empty, use single-line JSON | |
| */ | |
| 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) | |
| } | |
| /** | |
| * Format output based on compact mode - omit null/empty, use single-line JSON | |
| */ | |
| 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' && !Array.isArray(v) && Object.keys(v).length === 0) return false | |
| return true | |
| }) | |
| ) | |
| return JSON.stringify(filtered) | |
| } | |
| return JSON.stringify(data, null, 2) | |
| } |
🤖 Prompt for AI Agents
In `@packages/mcp/src/tools/snapshots.ts` around lines 15 - 31, The parameter type
for formatOutput should be changed from object to Record<string, unknown> to
avoid implicit any from Object.entries; update the function signature of
formatOutput and any related callers to accept Record<string, unknown>, and when
iterating Object.entries(data) treat each value as unknown and narrow it (e.g.,
use Array.isArray, v == null, typeof v === 'object' checks) before accessing
properties or Object.keys; specifically adjust the filtering callback around
Object.entries(data) and any type assertions so the code compiles under
noImplicitAny and Biome rules.
…tion
Add server mode system that defaults to compact mode for reduced token usage:
Summary by CodeRabbit
Release Notes
New Features
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.