Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/codemod-flag-removed-task-options.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/codemod': patch
---

The v1→v2 codemod no longer rewrites `taskStore`/`taskMessageQueue` McpServer constructor options into `capabilities.tasks` — that target does not exist in v2 (the experimental tasks runtime was removed, SEP-2663). The codemod now leaves the code untouched and emits an action-required diagnostic telling migrators to remove the option, matching the removal guidance already given for `experimental/tasks` imports and the migration guide.
10 changes: 0 additions & 10 deletions .changeset/extract-task-manager.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/fix-task-session-isolation.md

This file was deleted.

127 changes: 19 additions & 108 deletions packages/codemod/src/migrations/v1-to-v2/transforms/mcpServerApi.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -121,7 +121,7 @@ export const mcpServerApiTransform: Transform = {
}
}

changesCount += migrateConstructorTaskOptions(sourceFile, diagnostics);
flagRemovedTaskOptions(sourceFile, diagnostics);

return { changesCount, diagnostics };
}
Expand DownExpand Up@@ -414,11 +414,17 @@ function migrateResourceCall(call: CallExpression, _sourceFile: SourceFile): boo

const TASK_OPTIONS = ['taskStore', 'taskMessageQueue'] as const;

function migrateConstructorTaskOptions(sourceFile: SourceFile, diagnostics: Diagnostic[]): number {
/**
* Flag v1 task runtime options on the McpServer constructor as removed.
*
* The experimental tasks runtime was removed in v2 (SEP-2663) with no replacement, so
* these options cannot be migrated automatically. Emit an action-required diagnostic
* matching the importMap removal entry for `experimental/tasks`; the source is left
* untouched.
*/
function flagRemovedTaskOptions(sourceFile: SourceFile, diagnostics: Diagnostic[]): void {
const localName = resolveLocalImportName(sourceFile, 'McpServer');
if (!localName) return 0;

let changes = 0;
if (!localName) return;

for (const node of sourceFile.getDescendantsOfKind(SyntaxKind.NewExpression)) {
if (node.wasForgotten()) continue;
Expand All@@ -431,110 +437,15 @@ function migrateConstructorTaskOptions(sourceFile: SourceFile, diagnostics: Diag
const optionsArg = args[1]!;
if (!Node.isObjectLiteralExpression(optionsArg)) continue;

// Check if any task options are present at the top level
const propsToMove: string[] = [];
for (const propName of TASK_OPTIONS) {
if (optionsArg.getProperty(propName)) {
propsToMove.push(propName);
}
}
if (propsToMove.length === 0) continue;

// Find the tasks object's position within the options text using AST,
// then do all mutations via a single text replacement to avoid node invalidation.
const capabilitiesProp = optionsArg.getProperty('capabilities');
let tasksObjStart = -1;
let tasksObjEnd = -1;
const optionsStart = optionsArg.getStart();
if (capabilitiesProp && Node.isPropertyAssignment(capabilitiesProp)) {
const capInit = capabilitiesProp.getInitializer();
if (capInit && Node.isObjectLiteralExpression(capInit)) {
const tasksProp = capInit.getProperty('tasks');
if (tasksProp && Node.isPropertyAssignment(tasksProp)) {
const tasksInit = tasksProp.getInitializer();
if (tasksInit && Node.isObjectLiteralExpression(tasksInit)) {
tasksObjStart = tasksInit.getStart() - optionsStart;
tasksObjEnd = tasksInit.getEnd() - optionsStart;
}
}
}
}

if (tasksObjStart === -1) {
for (const propName of propsToMove) {
diagnostics.push(
actionRequired(
sourceFile.getFilePath(),
node,
`Move '${propName}' from McpServer options into capabilities.tasks — v2 expects task runtime options inside the tasks capability.`
)
);
}
continue;
}

// Single text replacement: remove top-level props and insert into tasks object.
// Use AST nodes (already located via getProperty) to get brace-balanced text and
// exact positions, avoiding regex truncation on values containing commas/braces.
// Collect all properties first, then process in reverse position order so each
// removal doesn't invalidate the positions of subsequent removals.
let optionsText = optionsArg.getText();
const argStart = optionsArg.getStart();
const propsWithPositions: { text: string; start: number; end: number }[] = [];
for (const propName of propsToMove) {
const prop = optionsArg.getProperty(propName);
if (!prop) continue;
propsWithPositions.push({
text: prop.getText(),
start: prop.getStart() - argStart,
end: prop.getEnd() - argStart
});
if (!optionsArg.getProperty(propName)) continue;
diagnostics.push(
actionRequired(
sourceFile.getFilePath(),
node,
`Remove '${propName}' from McpServer options — experimental tasks removed in v2 (SEP-2663 — tasks moved to the Extensions Track). No v2 equivalent.`
)
);
}
const propTexts = propsWithPositions.map(p => p.text);

// Remove in reverse position order so earlier positions remain valid
const sortedProps = propsWithPositions.toSorted((a, b) => b.start - a.start);
for (const { start, end } of sortedProps) {
let remStart = start;
let remEnd = end;
// Consume trailing comma and whitespace
const afterProp = optionsText.slice(remEnd);
const trailingMatch = afterProp.match(/^\s*,?\s*/);
if (trailingMatch) {
remEnd += trailingMatch[0].length;
}
// Consume leading whitespace/newline
const beforeProp = optionsText.slice(0, remStart);
const leadingMatch = beforeProp.match(/[\n\r]?\s*$/);
if (leadingMatch) {
remStart -= leadingMatch[0].length;
}
optionsText = optionsText.slice(0, remStart) + optionsText.slice(remEnd);
// Adjust tasks position if removal was before it
if (remStart < tasksObjStart) {
const shift = remEnd - remStart;
tasksObjStart -= shift;
tasksObjEnd -= shift;
}
}

if (propTexts.length === 0) continue;

// Insert into the tasks object (just before its closing brace)
const tasksText = optionsText.slice(tasksObjStart, tasksObjEnd);
const closingBrace = tasksText.lastIndexOf('}');
const before = tasksText.slice(0, closingBrace).trimEnd();
const sep = before.length > 1 ? ',\n' : '\n';
const newTasksText = before + sep + propTexts.join(',\n') + '\n' + tasksText.slice(closingBrace);
optionsText = optionsText.slice(0, tasksObjStart) + newTasksText + optionsText.slice(tasksObjEnd);

// Clean up double/trailing commas
optionsText = optionsText.replaceAll(/,(\s*,)/g, ',');
optionsText = optionsText.replaceAll(/,(\s*})/g, '$1');

optionsArg.replaceWithText(optionsText);
changes += propTexts.length;
}

return changes;
}
59 changes: 59 additions & 0 deletions packages/codemod/test/v1-to-v2/transforms/mcpServerApi.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -307,4 +307,63 @@ describe('mcp-server-api transform', () => {
expect(result).toContain('registerTool("ping", {}');
expect(result).not.toContain('z.object');
});

it('flags taskStore in McpServer options as removed without modifying code', () => {
const input = [
`const server = new McpServer(`,
` { name: 'test', version: '1.0' },`,
` { taskStore: new InMemoryTaskStore() }`,
`);`,
''
].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input);
const taskDiags = result.diagnostics.filter(d => d.message.includes("'taskStore'"));
expect(taskDiags).toHaveLength(1);
expect(taskDiags[0]!.message).toContain('experimental tasks removed in v2 (SEP-2663');
expect(taskDiags[0]!.message).toContain('No v2 equivalent');
expect(taskDiags[0]!.insertComment).toBe(true);
});

it('flags each task option separately when both are present', () => {
const input = [
`const server = new McpServer(`,
` { name: 'test', version: '1.0' },`,
` { taskStore: store, taskMessageQueue: queue }`,
`);`,
''
].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input);
expect(result.diagnostics.some(d => d.message.includes("'taskStore'"))).toBe(true);
expect(result.diagnostics.some(d => d.message.includes("'taskMessageQueue'"))).toBe(true);
});

it('does not move task options into capabilities.tasks even when present', () => {
const input = [
`const server = new McpServer(`,
` { name: 'test', version: '1.0' },`,
` { taskStore: store, capabilities: { tasks: {} } }`,
`);`,
''
].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input);
expect(sourceFile.getFullText()).toContain('taskStore: store');
expect(result.diagnostics.some(d => d.message.includes("'taskStore'"))).toBe(true);
});

it('emits no task diagnostics for McpServer options without task options', () => {
const input = [`const server = new McpServer({ name: 'test', version: '1.0' }, { instructions: 'hi' });`, ''].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(result.diagnostics).toHaveLength(0);
});
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(codemod): flag removed task options instead of migrating them; drop stale task artifacts by felixweinberger · Pull Request #2288 · modelcontextprotocol/typescript-sdk · GitHub
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/codemod-flag-removed-task-options.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/codemod': patch
---

The v1→v2 codemod no longer rewrites `taskStore`/`taskMessageQueue` McpServer constructor options into `capabilities.tasks` — that target does not exist in v2 (the experimental tasks runtime was removed, SEP-2663). The codemod now leaves the code untouched and emits an action-required diagnostic telling migrators to remove the option, matching the removal guidance already given for `experimental/tasks` imports and the migration guide.
10 changes: 0 additions & 10 deletions .changeset/extract-task-manager.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/fix-task-session-isolation.md

This file was deleted.

127 changes: 19 additions & 108 deletions packages/codemod/src/migrations/v1-to-v2/transforms/mcpServerApi.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -121,7 +121,7 @@ export const mcpServerApiTransform: Transform = {
}
}

changesCount += migrateConstructorTaskOptions(sourceFile, diagnostics);
flagRemovedTaskOptions(sourceFile, diagnostics);

return { changesCount, diagnostics };
}
Expand DownExpand Up@@ -414,11 +414,17 @@ function migrateResourceCall(call: CallExpression, _sourceFile: SourceFile): boo

const TASK_OPTIONS = ['taskStore', 'taskMessageQueue'] as const;

function migrateConstructorTaskOptions(sourceFile: SourceFile, diagnostics: Diagnostic[]): number {
/**
* Flag v1 task runtime options on the McpServer constructor as removed.
*
* The experimental tasks runtime was removed in v2 (SEP-2663) with no replacement, so
* these options cannot be migrated automatically. Emit an action-required diagnostic
* matching the importMap removal entry for `experimental/tasks`; the source is left
* untouched.
*/
function flagRemovedTaskOptions(sourceFile: SourceFile, diagnostics: Diagnostic[]): void {
const localName = resolveLocalImportName(sourceFile, 'McpServer');
if (!localName) return 0;

let changes = 0;
if (!localName) return;

for (const node of sourceFile.getDescendantsOfKind(SyntaxKind.NewExpression)) {
if (node.wasForgotten()) continue;
Expand All@@ -431,110 +437,15 @@ function migrateConstructorTaskOptions(sourceFile: SourceFile, diagnostics: Diag
const optionsArg = args[1]!;
if (!Node.isObjectLiteralExpression(optionsArg)) continue;

// Check if any task options are present at the top level
const propsToMove: string[] = [];
for (const propName of TASK_OPTIONS) {
if (optionsArg.getProperty(propName)) {
propsToMove.push(propName);
}
}
if (propsToMove.length === 0) continue;

// Find the tasks object's position within the options text using AST,
// then do all mutations via a single text replacement to avoid node invalidation.
const capabilitiesProp = optionsArg.getProperty('capabilities');
let tasksObjStart = -1;
let tasksObjEnd = -1;
const optionsStart = optionsArg.getStart();
if (capabilitiesProp && Node.isPropertyAssignment(capabilitiesProp)) {
const capInit = capabilitiesProp.getInitializer();
if (capInit && Node.isObjectLiteralExpression(capInit)) {
const tasksProp = capInit.getProperty('tasks');
if (tasksProp && Node.isPropertyAssignment(tasksProp)) {
const tasksInit = tasksProp.getInitializer();
if (tasksInit && Node.isObjectLiteralExpression(tasksInit)) {
tasksObjStart = tasksInit.getStart() - optionsStart;
tasksObjEnd = tasksInit.getEnd() - optionsStart;
}
}
}
}

if (tasksObjStart === -1) {
for (const propName of propsToMove) {
diagnostics.push(
actionRequired(
sourceFile.getFilePath(),
node,
`Move '${propName}' from McpServer options into capabilities.tasks — v2 expects task runtime options inside the tasks capability.`
)
);
}
continue;
}

// Single text replacement: remove top-level props and insert into tasks object.
// Use AST nodes (already located via getProperty) to get brace-balanced text and
// exact positions, avoiding regex truncation on values containing commas/braces.
// Collect all properties first, then process in reverse position order so each
// removal doesn't invalidate the positions of subsequent removals.
let optionsText = optionsArg.getText();
const argStart = optionsArg.getStart();
const propsWithPositions: { text: string; start: number; end: number }[] = [];
for (const propName of propsToMove) {
const prop = optionsArg.getProperty(propName);
if (!prop) continue;
propsWithPositions.push({
text: prop.getText(),
start: prop.getStart() - argStart,
end: prop.getEnd() - argStart
});
if (!optionsArg.getProperty(propName)) continue;
diagnostics.push(
actionRequired(
sourceFile.getFilePath(),
node,
`Remove '${propName}' from McpServer options — experimental tasks removed in v2 (SEP-2663 — tasks moved to the Extensions Track). No v2 equivalent.`
)
);
}
const propTexts = propsWithPositions.map(p => p.text);

// Remove in reverse position order so earlier positions remain valid
const sortedProps = propsWithPositions.toSorted((a, b) => b.start - a.start);
for (const { start, end } of sortedProps) {
let remStart = start;
let remEnd = end;
// Consume trailing comma and whitespace
const afterProp = optionsText.slice(remEnd);
const trailingMatch = afterProp.match(/^\s*,?\s*/);
if (trailingMatch) {
remEnd += trailingMatch[0].length;
}
// Consume leading whitespace/newline
const beforeProp = optionsText.slice(0, remStart);
const leadingMatch = beforeProp.match(/[\n\r]?\s*$/);
if (leadingMatch) {
remStart -= leadingMatch[0].length;
}
optionsText = optionsText.slice(0, remStart) + optionsText.slice(remEnd);
// Adjust tasks position if removal was before it
if (remStart < tasksObjStart) {
const shift = remEnd - remStart;
tasksObjStart -= shift;
tasksObjEnd -= shift;
}
}

if (propTexts.length === 0) continue;

// Insert into the tasks object (just before its closing brace)
const tasksText = optionsText.slice(tasksObjStart, tasksObjEnd);
const closingBrace = tasksText.lastIndexOf('}');
const before = tasksText.slice(0, closingBrace).trimEnd();
const sep = before.length > 1 ? ',\n' : '\n';
const newTasksText = before + sep + propTexts.join(',\n') + '\n' + tasksText.slice(closingBrace);
optionsText = optionsText.slice(0, tasksObjStart) + newTasksText + optionsText.slice(tasksObjEnd);

// Clean up double/trailing commas
optionsText = optionsText.replaceAll(/,(\s*,)/g, ',');
optionsText = optionsText.replaceAll(/,(\s*})/g, '$1');

optionsArg.replaceWithText(optionsText);
changes += propTexts.length;
}

return changes;
}
59 changes: 59 additions & 0 deletions packages/codemod/test/v1-to-v2/transforms/mcpServerApi.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -307,4 +307,63 @@ describe('mcp-server-api transform', () => {
expect(result).toContain('registerTool("ping", {}');
expect(result).not.toContain('z.object');
});

it('flags taskStore in McpServer options as removed without modifying code', () => {
const input = [
`const server = new McpServer(`,
` { name: 'test', version: '1.0' },`,
` { taskStore: new InMemoryTaskStore() }`,
`);`,
''
].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input);
const taskDiags = result.diagnostics.filter(d => d.message.includes("'taskStore'"));
expect(taskDiags).toHaveLength(1);
expect(taskDiags[0]!.message).toContain('experimental tasks removed in v2 (SEP-2663');
expect(taskDiags[0]!.message).toContain('No v2 equivalent');
expect(taskDiags[0]!.insertComment).toBe(true);
});

it('flags each task option separately when both are present', () => {
const input = [
`const server = new McpServer(`,
` { name: 'test', version: '1.0' },`,
` { taskStore: store, taskMessageQueue: queue }`,
`);`,
''
].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input);
expect(result.diagnostics.some(d => d.message.includes("'taskStore'"))).toBe(true);
expect(result.diagnostics.some(d => d.message.includes("'taskMessageQueue'"))).toBe(true);
});

it('does not move task options into capabilities.tasks even when present', () => {
const input = [
`const server = new McpServer(`,
` { name: 'test', version: '1.0' },`,
` { taskStore: store, capabilities: { tasks: {} } }`,
`);`,
''
].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input);
expect(sourceFile.getFullText()).toContain('taskStore: store');
expect(result.diagnostics.some(d => d.message.includes("'taskStore'"))).toBe(true);
});

it('emits no task diagnostics for McpServer options without task options', () => {
const input = [`const server = new McpServer({ name: 'test', version: '1.0' }, { instructions: 'hi' });`, ''].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(result.diagnostics).toHaveLength(0);
});
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(codemod): flag removed task options instead of migrating them; drop stale task artifacts by felixweinberger · Pull Request #2288 · modelcontextprotocol/typescript-sdk · GitHub
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/codemod-flag-removed-task-options.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/codemod': patch
---

The v1→v2 codemod no longer rewrites `taskStore`/`taskMessageQueue` McpServer constructor options into `capabilities.tasks` — that target does not exist in v2 (the experimental tasks runtime was removed, SEP-2663). The codemod now leaves the code untouched and emits an action-required diagnostic telling migrators to remove the option, matching the removal guidance already given for `experimental/tasks` imports and the migration guide.
10 changes: 0 additions & 10 deletions .changeset/extract-task-manager.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/fix-task-session-isolation.md

This file was deleted.

127 changes: 19 additions & 108 deletions packages/codemod/src/migrations/v1-to-v2/transforms/mcpServerApi.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -121,7 +121,7 @@ export const mcpServerApiTransform: Transform = {
}
}

changesCount += migrateConstructorTaskOptions(sourceFile, diagnostics);
flagRemovedTaskOptions(sourceFile, diagnostics);

return { changesCount, diagnostics };
}
Expand DownExpand Up@@ -414,11 +414,17 @@ function migrateResourceCall(call: CallExpression, _sourceFile: SourceFile): boo

const TASK_OPTIONS = ['taskStore', 'taskMessageQueue'] as const;

function migrateConstructorTaskOptions(sourceFile: SourceFile, diagnostics: Diagnostic[]): number {
/**
* Flag v1 task runtime options on the McpServer constructor as removed.
*
* The experimental tasks runtime was removed in v2 (SEP-2663) with no replacement, so
* these options cannot be migrated automatically. Emit an action-required diagnostic
* matching the importMap removal entry for `experimental/tasks`; the source is left
* untouched.
*/
function flagRemovedTaskOptions(sourceFile: SourceFile, diagnostics: Diagnostic[]): void {
const localName = resolveLocalImportName(sourceFile, 'McpServer');
if (!localName) return 0;

let changes = 0;
if (!localName) return;

for (const node of sourceFile.getDescendantsOfKind(SyntaxKind.NewExpression)) {
if (node.wasForgotten()) continue;
Expand All@@ -431,110 +437,15 @@ function migrateConstructorTaskOptions(sourceFile: SourceFile, diagnostics: Diag
const optionsArg = args[1]!;
if (!Node.isObjectLiteralExpression(optionsArg)) continue;

// Check if any task options are present at the top level
const propsToMove: string[] = [];
for (const propName of TASK_OPTIONS) {
if (optionsArg.getProperty(propName)) {
propsToMove.push(propName);
}
}
if (propsToMove.length === 0) continue;

// Find the tasks object's position within the options text using AST,
// then do all mutations via a single text replacement to avoid node invalidation.
const capabilitiesProp = optionsArg.getProperty('capabilities');
let tasksObjStart = -1;
let tasksObjEnd = -1;
const optionsStart = optionsArg.getStart();
if (capabilitiesProp && Node.isPropertyAssignment(capabilitiesProp)) {
const capInit = capabilitiesProp.getInitializer();
if (capInit && Node.isObjectLiteralExpression(capInit)) {
const tasksProp = capInit.getProperty('tasks');
if (tasksProp && Node.isPropertyAssignment(tasksProp)) {
const tasksInit = tasksProp.getInitializer();
if (tasksInit && Node.isObjectLiteralExpression(tasksInit)) {
tasksObjStart = tasksInit.getStart() - optionsStart;
tasksObjEnd = tasksInit.getEnd() - optionsStart;
}
}
}
}

if (tasksObjStart === -1) {
for (const propName of propsToMove) {
diagnostics.push(
actionRequired(
sourceFile.getFilePath(),
node,
`Move '${propName}' from McpServer options into capabilities.tasks — v2 expects task runtime options inside the tasks capability.`
)
);
}
continue;
}

// Single text replacement: remove top-level props and insert into tasks object.
// Use AST nodes (already located via getProperty) to get brace-balanced text and
// exact positions, avoiding regex truncation on values containing commas/braces.
// Collect all properties first, then process in reverse position order so each
// removal doesn't invalidate the positions of subsequent removals.
let optionsText = optionsArg.getText();
const argStart = optionsArg.getStart();
const propsWithPositions: { text: string; start: number; end: number }[] = [];
for (const propName of propsToMove) {
const prop = optionsArg.getProperty(propName);
if (!prop) continue;
propsWithPositions.push({
text: prop.getText(),
start: prop.getStart() - argStart,
end: prop.getEnd() - argStart
});
if (!optionsArg.getProperty(propName)) continue;
diagnostics.push(
actionRequired(
sourceFile.getFilePath(),
node,
`Remove '${propName}' from McpServer options — experimental tasks removed in v2 (SEP-2663 — tasks moved to the Extensions Track). No v2 equivalent.`
)
);
}
const propTexts = propsWithPositions.map(p => p.text);

// Remove in reverse position order so earlier positions remain valid
const sortedProps = propsWithPositions.toSorted((a, b) => b.start - a.start);
for (const { start, end } of sortedProps) {
let remStart = start;
let remEnd = end;
// Consume trailing comma and whitespace
const afterProp = optionsText.slice(remEnd);
const trailingMatch = afterProp.match(/^\s*,?\s*/);
if (trailingMatch) {
remEnd += trailingMatch[0].length;
}
// Consume leading whitespace/newline
const beforeProp = optionsText.slice(0, remStart);
const leadingMatch = beforeProp.match(/[\n\r]?\s*$/);
if (leadingMatch) {
remStart -= leadingMatch[0].length;
}
optionsText = optionsText.slice(0, remStart) + optionsText.slice(remEnd);
// Adjust tasks position if removal was before it
if (remStart < tasksObjStart) {
const shift = remEnd - remStart;
tasksObjStart -= shift;
tasksObjEnd -= shift;
}
}

if (propTexts.length === 0) continue;

// Insert into the tasks object (just before its closing brace)
const tasksText = optionsText.slice(tasksObjStart, tasksObjEnd);
const closingBrace = tasksText.lastIndexOf('}');
const before = tasksText.slice(0, closingBrace).trimEnd();
const sep = before.length > 1 ? ',\n' : '\n';
const newTasksText = before + sep + propTexts.join(',\n') + '\n' + tasksText.slice(closingBrace);
optionsText = optionsText.slice(0, tasksObjStart) + newTasksText + optionsText.slice(tasksObjEnd);

// Clean up double/trailing commas
optionsText = optionsText.replaceAll(/,(\s*,)/g, ',');
optionsText = optionsText.replaceAll(/,(\s*})/g, '$1');

optionsArg.replaceWithText(optionsText);
changes += propTexts.length;
}

return changes;
}
59 changes: 59 additions & 0 deletions packages/codemod/test/v1-to-v2/transforms/mcpServerApi.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -307,4 +307,63 @@ describe('mcp-server-api transform', () => {
expect(result).toContain('registerTool("ping", {}');
expect(result).not.toContain('z.object');
});

it('flags taskStore in McpServer options as removed without modifying code', () => {
const input = [
`const server = new McpServer(`,
` { name: 'test', version: '1.0' },`,
` { taskStore: new InMemoryTaskStore() }`,
`);`,
''
].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input);
const taskDiags = result.diagnostics.filter(d => d.message.includes("'taskStore'"));
expect(taskDiags).toHaveLength(1);
expect(taskDiags[0]!.message).toContain('experimental tasks removed in v2 (SEP-2663');
expect(taskDiags[0]!.message).toContain('No v2 equivalent');
expect(taskDiags[0]!.insertComment).toBe(true);
});

it('flags each task option separately when both are present', () => {
const input = [
`const server = new McpServer(`,
` { name: 'test', version: '1.0' },`,
` { taskStore: store, taskMessageQueue: queue }`,
`);`,
''
].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input);
expect(result.diagnostics.some(d => d.message.includes("'taskStore'"))).toBe(true);
expect(result.diagnostics.some(d => d.message.includes("'taskMessageQueue'"))).toBe(true);
});

it('does not move task options into capabilities.tasks even when present', () => {
const input = [
`const server = new McpServer(`,
` { name: 'test', version: '1.0' },`,
` { taskStore: store, capabilities: { tasks: {} } }`,
`);`,
''
].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input);
expect(sourceFile.getFullText()).toContain('taskStore: store');
expect(result.diagnostics.some(d => d.message.includes("'taskStore'"))).toBe(true);
});

it('emits no task diagnostics for McpServer options without task options', () => {
const input = [`const server = new McpServer({ name: 'test', version: '1.0' }, { instructions: 'hi' });`, ''].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(result.diagnostics).toHaveLength(0);
});
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(codemod): flag removed task options instead of migrating them; drop stale task artifacts by felixweinberger · Pull Request #2288 · modelcontextprotocol/typescript-sdk · GitHub
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/codemod-flag-removed-task-options.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/codemod': patch
---

The v1→v2 codemod no longer rewrites `taskStore`/`taskMessageQueue` McpServer constructor options into `capabilities.tasks` — that target does not exist in v2 (the experimental tasks runtime was removed, SEP-2663). The codemod now leaves the code untouched and emits an action-required diagnostic telling migrators to remove the option, matching the removal guidance already given for `experimental/tasks` imports and the migration guide.
10 changes: 0 additions & 10 deletions .changeset/extract-task-manager.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/fix-task-session-isolation.md

This file was deleted.

127 changes: 19 additions & 108 deletions packages/codemod/src/migrations/v1-to-v2/transforms/mcpServerApi.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -121,7 +121,7 @@ export const mcpServerApiTransform: Transform = {
}
}

changesCount += migrateConstructorTaskOptions(sourceFile, diagnostics);
flagRemovedTaskOptions(sourceFile, diagnostics);

return { changesCount, diagnostics };
}
Expand DownExpand Up@@ -414,11 +414,17 @@ function migrateResourceCall(call: CallExpression, _sourceFile: SourceFile): boo

const TASK_OPTIONS = ['taskStore', 'taskMessageQueue'] as const;

function migrateConstructorTaskOptions(sourceFile: SourceFile, diagnostics: Diagnostic[]): number {
/**
* Flag v1 task runtime options on the McpServer constructor as removed.
*
* The experimental tasks runtime was removed in v2 (SEP-2663) with no replacement, so
* these options cannot be migrated automatically. Emit an action-required diagnostic
* matching the importMap removal entry for `experimental/tasks`; the source is left
* untouched.
*/
function flagRemovedTaskOptions(sourceFile: SourceFile, diagnostics: Diagnostic[]): void {
const localName = resolveLocalImportName(sourceFile, 'McpServer');
if (!localName) return 0;

let changes = 0;
if (!localName) return;

for (const node of sourceFile.getDescendantsOfKind(SyntaxKind.NewExpression)) {
if (node.wasForgotten()) continue;
Expand All@@ -431,110 +437,15 @@ function migrateConstructorTaskOptions(sourceFile: SourceFile, diagnostics: Diag
const optionsArg = args[1]!;
if (!Node.isObjectLiteralExpression(optionsArg)) continue;

// Check if any task options are present at the top level
const propsToMove: string[] = [];
for (const propName of TASK_OPTIONS) {
if (optionsArg.getProperty(propName)) {
propsToMove.push(propName);
}
}
if (propsToMove.length === 0) continue;

// Find the tasks object's position within the options text using AST,
// then do all mutations via a single text replacement to avoid node invalidation.
const capabilitiesProp = optionsArg.getProperty('capabilities');
let tasksObjStart = -1;
let tasksObjEnd = -1;
const optionsStart = optionsArg.getStart();
if (capabilitiesProp && Node.isPropertyAssignment(capabilitiesProp)) {
const capInit = capabilitiesProp.getInitializer();
if (capInit && Node.isObjectLiteralExpression(capInit)) {
const tasksProp = capInit.getProperty('tasks');
if (tasksProp && Node.isPropertyAssignment(tasksProp)) {
const tasksInit = tasksProp.getInitializer();
if (tasksInit && Node.isObjectLiteralExpression(tasksInit)) {
tasksObjStart = tasksInit.getStart() - optionsStart;
tasksObjEnd = tasksInit.getEnd() - optionsStart;
}
}
}
}

if (tasksObjStart === -1) {
for (const propName of propsToMove) {
diagnostics.push(
actionRequired(
sourceFile.getFilePath(),
node,
`Move '${propName}' from McpServer options into capabilities.tasks — v2 expects task runtime options inside the tasks capability.`
)
);
}
continue;
}

// Single text replacement: remove top-level props and insert into tasks object.
// Use AST nodes (already located via getProperty) to get brace-balanced text and
// exact positions, avoiding regex truncation on values containing commas/braces.
// Collect all properties first, then process in reverse position order so each
// removal doesn't invalidate the positions of subsequent removals.
let optionsText = optionsArg.getText();
const argStart = optionsArg.getStart();
const propsWithPositions: { text: string; start: number; end: number }[] = [];
for (const propName of propsToMove) {
const prop = optionsArg.getProperty(propName);
if (!prop) continue;
propsWithPositions.push({
text: prop.getText(),
start: prop.getStart() - argStart,
end: prop.getEnd() - argStart
});
if (!optionsArg.getProperty(propName)) continue;
diagnostics.push(
actionRequired(
sourceFile.getFilePath(),
node,
`Remove '${propName}' from McpServer options — experimental tasks removed in v2 (SEP-2663 — tasks moved to the Extensions Track). No v2 equivalent.`
)
);
}
const propTexts = propsWithPositions.map(p => p.text);

// Remove in reverse position order so earlier positions remain valid
const sortedProps = propsWithPositions.toSorted((a, b) => b.start - a.start);
for (const { start, end } of sortedProps) {
let remStart = start;
let remEnd = end;
// Consume trailing comma and whitespace
const afterProp = optionsText.slice(remEnd);
const trailingMatch = afterProp.match(/^\s*,?\s*/);
if (trailingMatch) {
remEnd += trailingMatch[0].length;
}
// Consume leading whitespace/newline
const beforeProp = optionsText.slice(0, remStart);
const leadingMatch = beforeProp.match(/[\n\r]?\s*$/);
if (leadingMatch) {
remStart -= leadingMatch[0].length;
}
optionsText = optionsText.slice(0, remStart) + optionsText.slice(remEnd);
// Adjust tasks position if removal was before it
if (remStart < tasksObjStart) {
const shift = remEnd - remStart;
tasksObjStart -= shift;
tasksObjEnd -= shift;
}
}

if (propTexts.length === 0) continue;

// Insert into the tasks object (just before its closing brace)
const tasksText = optionsText.slice(tasksObjStart, tasksObjEnd);
const closingBrace = tasksText.lastIndexOf('}');
const before = tasksText.slice(0, closingBrace).trimEnd();
const sep = before.length > 1 ? ',\n' : '\n';
const newTasksText = before + sep + propTexts.join(',\n') + '\n' + tasksText.slice(closingBrace);
optionsText = optionsText.slice(0, tasksObjStart) + newTasksText + optionsText.slice(tasksObjEnd);

// Clean up double/trailing commas
optionsText = optionsText.replaceAll(/,(\s*,)/g, ',');
optionsText = optionsText.replaceAll(/,(\s*})/g, '$1');

optionsArg.replaceWithText(optionsText);
changes += propTexts.length;
}

return changes;
}
59 changes: 59 additions & 0 deletions packages/codemod/test/v1-to-v2/transforms/mcpServerApi.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -307,4 +307,63 @@ describe('mcp-server-api transform', () => {
expect(result).toContain('registerTool("ping", {}');
expect(result).not.toContain('z.object');
});

it('flags taskStore in McpServer options as removed without modifying code', () => {
const input = [
`const server = new McpServer(`,
` { name: 'test', version: '1.0' },`,
` { taskStore: new InMemoryTaskStore() }`,
`);`,
''
].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input);
const taskDiags = result.diagnostics.filter(d => d.message.includes("'taskStore'"));
expect(taskDiags).toHaveLength(1);
expect(taskDiags[0]!.message).toContain('experimental tasks removed in v2 (SEP-2663');
expect(taskDiags[0]!.message).toContain('No v2 equivalent');
expect(taskDiags[0]!.insertComment).toBe(true);
});

it('flags each task option separately when both are present', () => {
const input = [
`const server = new McpServer(`,
` { name: 'test', version: '1.0' },`,
` { taskStore: store, taskMessageQueue: queue }`,
`);`,
''
].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input);
expect(result.diagnostics.some(d => d.message.includes("'taskStore'"))).toBe(true);
expect(result.diagnostics.some(d => d.message.includes("'taskMessageQueue'"))).toBe(true);
});

it('does not move task options into capabilities.tasks even when present', () => {
const input = [
`const server = new McpServer(`,
` { name: 'test', version: '1.0' },`,
` { taskStore: store, capabilities: { tasks: {} } }`,
`);`,
''
].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input);
expect(sourceFile.getFullText()).toContain('taskStore: store');
expect(result.diagnostics.some(d => d.message.includes("'taskStore'"))).toBe(true);
});

it('emits no task diagnostics for McpServer options without task options', () => {
const input = [`const server = new McpServer({ name: 'test', version: '1.0' }, { instructions: 'hi' });`, ''].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(result.diagnostics).toHaveLength(0);
});
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(codemod): flag removed task options instead of migrating them; drop stale task artifacts by felixweinberger · Pull Request #2288 · modelcontextprotocol/typescript-sdk · GitHub
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/codemod-flag-removed-task-options.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/codemod': patch
---

The v1→v2 codemod no longer rewrites `taskStore`/`taskMessageQueue` McpServer constructor options into `capabilities.tasks` — that target does not exist in v2 (the experimental tasks runtime was removed, SEP-2663). The codemod now leaves the code untouched and emits an action-required diagnostic telling migrators to remove the option, matching the removal guidance already given for `experimental/tasks` imports and the migration guide.
10 changes: 0 additions & 10 deletions .changeset/extract-task-manager.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/fix-task-session-isolation.md

This file was deleted.

127 changes: 19 additions & 108 deletions packages/codemod/src/migrations/v1-to-v2/transforms/mcpServerApi.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -121,7 +121,7 @@ export const mcpServerApiTransform: Transform = {
}
}

changesCount += migrateConstructorTaskOptions(sourceFile, diagnostics);
flagRemovedTaskOptions(sourceFile, diagnostics);

return { changesCount, diagnostics };
}
Expand DownExpand Up@@ -414,11 +414,17 @@ function migrateResourceCall(call: CallExpression, _sourceFile: SourceFile): boo

const TASK_OPTIONS = ['taskStore', 'taskMessageQueue'] as const;

function migrateConstructorTaskOptions(sourceFile: SourceFile, diagnostics: Diagnostic[]): number {
/**
* Flag v1 task runtime options on the McpServer constructor as removed.
*
* The experimental tasks runtime was removed in v2 (SEP-2663) with no replacement, so
* these options cannot be migrated automatically. Emit an action-required diagnostic
* matching the importMap removal entry for `experimental/tasks`; the source is left
* untouched.
*/
function flagRemovedTaskOptions(sourceFile: SourceFile, diagnostics: Diagnostic[]): void {
const localName = resolveLocalImportName(sourceFile, 'McpServer');
if (!localName) return 0;

let changes = 0;
if (!localName) return;

for (const node of sourceFile.getDescendantsOfKind(SyntaxKind.NewExpression)) {
if (node.wasForgotten()) continue;
Expand All@@ -431,110 +437,15 @@ function migrateConstructorTaskOptions(sourceFile: SourceFile, diagnostics: Diag
const optionsArg = args[1]!;
if (!Node.isObjectLiteralExpression(optionsArg)) continue;

// Check if any task options are present at the top level
const propsToMove: string[] = [];
for (const propName of TASK_OPTIONS) {
if (optionsArg.getProperty(propName)) {
propsToMove.push(propName);
}
}
if (propsToMove.length === 0) continue;

// Find the tasks object's position within the options text using AST,
// then do all mutations via a single text replacement to avoid node invalidation.
const capabilitiesProp = optionsArg.getProperty('capabilities');
let tasksObjStart = -1;
let tasksObjEnd = -1;
const optionsStart = optionsArg.getStart();
if (capabilitiesProp && Node.isPropertyAssignment(capabilitiesProp)) {
const capInit = capabilitiesProp.getInitializer();
if (capInit && Node.isObjectLiteralExpression(capInit)) {
const tasksProp = capInit.getProperty('tasks');
if (tasksProp && Node.isPropertyAssignment(tasksProp)) {
const tasksInit = tasksProp.getInitializer();
if (tasksInit && Node.isObjectLiteralExpression(tasksInit)) {
tasksObjStart = tasksInit.getStart() - optionsStart;
tasksObjEnd = tasksInit.getEnd() - optionsStart;
}
}
}
}

if (tasksObjStart === -1) {
for (const propName of propsToMove) {
diagnostics.push(
actionRequired(
sourceFile.getFilePath(),
node,
`Move '${propName}' from McpServer options into capabilities.tasks — v2 expects task runtime options inside the tasks capability.`
)
);
}
continue;
}

// Single text replacement: remove top-level props and insert into tasks object.
// Use AST nodes (already located via getProperty) to get brace-balanced text and
// exact positions, avoiding regex truncation on values containing commas/braces.
// Collect all properties first, then process in reverse position order so each
// removal doesn't invalidate the positions of subsequent removals.
let optionsText = optionsArg.getText();
const argStart = optionsArg.getStart();
const propsWithPositions: { text: string; start: number; end: number }[] = [];
for (const propName of propsToMove) {
const prop = optionsArg.getProperty(propName);
if (!prop) continue;
propsWithPositions.push({
text: prop.getText(),
start: prop.getStart() - argStart,
end: prop.getEnd() - argStart
});
if (!optionsArg.getProperty(propName)) continue;
diagnostics.push(
actionRequired(
sourceFile.getFilePath(),
node,
`Remove '${propName}' from McpServer options — experimental tasks removed in v2 (SEP-2663 — tasks moved to the Extensions Track). No v2 equivalent.`
)
);
}
const propTexts = propsWithPositions.map(p => p.text);

// Remove in reverse position order so earlier positions remain valid
const sortedProps = propsWithPositions.toSorted((a, b) => b.start - a.start);
for (const { start, end } of sortedProps) {
let remStart = start;
let remEnd = end;
// Consume trailing comma and whitespace
const afterProp = optionsText.slice(remEnd);
const trailingMatch = afterProp.match(/^\s*,?\s*/);
if (trailingMatch) {
remEnd += trailingMatch[0].length;
}
// Consume leading whitespace/newline
const beforeProp = optionsText.slice(0, remStart);
const leadingMatch = beforeProp.match(/[\n\r]?\s*$/);
if (leadingMatch) {
remStart -= leadingMatch[0].length;
}
optionsText = optionsText.slice(0, remStart) + optionsText.slice(remEnd);
// Adjust tasks position if removal was before it
if (remStart < tasksObjStart) {
const shift = remEnd - remStart;
tasksObjStart -= shift;
tasksObjEnd -= shift;
}
}

if (propTexts.length === 0) continue;

// Insert into the tasks object (just before its closing brace)
const tasksText = optionsText.slice(tasksObjStart, tasksObjEnd);
const closingBrace = tasksText.lastIndexOf('}');
const before = tasksText.slice(0, closingBrace).trimEnd();
const sep = before.length > 1 ? ',\n' : '\n';
const newTasksText = before + sep + propTexts.join(',\n') + '\n' + tasksText.slice(closingBrace);
optionsText = optionsText.slice(0, tasksObjStart) + newTasksText + optionsText.slice(tasksObjEnd);

// Clean up double/trailing commas
optionsText = optionsText.replaceAll(/,(\s*,)/g, ',');
optionsText = optionsText.replaceAll(/,(\s*})/g, '$1');

optionsArg.replaceWithText(optionsText);
changes += propTexts.length;
}

return changes;
}
59 changes: 59 additions & 0 deletions packages/codemod/test/v1-to-v2/transforms/mcpServerApi.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -307,4 +307,63 @@ describe('mcp-server-api transform', () => {
expect(result).toContain('registerTool("ping", {}');
expect(result).not.toContain('z.object');
});

it('flags taskStore in McpServer options as removed without modifying code', () => {
const input = [
`const server = new McpServer(`,
` { name: 'test', version: '1.0' },`,
` { taskStore: new InMemoryTaskStore() }`,
`);`,
''
].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input);
const taskDiags = result.diagnostics.filter(d => d.message.includes("'taskStore'"));
expect(taskDiags).toHaveLength(1);
expect(taskDiags[0]!.message).toContain('experimental tasks removed in v2 (SEP-2663');
expect(taskDiags[0]!.message).toContain('No v2 equivalent');
expect(taskDiags[0]!.insertComment).toBe(true);
});

it('flags each task option separately when both are present', () => {
const input = [
`const server = new McpServer(`,
` { name: 'test', version: '1.0' },`,
` { taskStore: store, taskMessageQueue: queue }`,
`);`,
''
].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input);
expect(result.diagnostics.some(d => d.message.includes("'taskStore'"))).toBe(true);
expect(result.diagnostics.some(d => d.message.includes("'taskMessageQueue'"))).toBe(true);
});

it('does not move task options into capabilities.tasks even when present', () => {
const input = [
`const server = new McpServer(`,
` { name: 'test', version: '1.0' },`,
` { taskStore: store, capabilities: { tasks: {} } }`,
`);`,
''
].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input);
expect(sourceFile.getFullText()).toContain('taskStore: store');
expect(result.diagnostics.some(d => d.message.includes("'taskStore'"))).toBe(true);
});

it('emits no task diagnostics for McpServer options without task options', () => {
const input = [`const server = new McpServer({ name: 'test', version: '1.0' }, { instructions: 'hi' });`, ''].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(result.diagnostics).toHaveLength(0);
});
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(codemod): flag removed task options instead of migrating them; drop stale task artifacts by felixweinberger · Pull Request #2288 · modelcontextprotocol/typescript-sdk · GitHub
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/codemod-flag-removed-task-options.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/codemod': patch
---

The v1→v2 codemod no longer rewrites `taskStore`/`taskMessageQueue` McpServer constructor options into `capabilities.tasks` — that target does not exist in v2 (the experimental tasks runtime was removed, SEP-2663). The codemod now leaves the code untouched and emits an action-required diagnostic telling migrators to remove the option, matching the removal guidance already given for `experimental/tasks` imports and the migration guide.
10 changes: 0 additions & 10 deletions .changeset/extract-task-manager.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/fix-task-session-isolation.md

This file was deleted.

127 changes: 19 additions & 108 deletions packages/codemod/src/migrations/v1-to-v2/transforms/mcpServerApi.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -121,7 +121,7 @@ export const mcpServerApiTransform: Transform = {
}
}

changesCount += migrateConstructorTaskOptions(sourceFile, diagnostics);
flagRemovedTaskOptions(sourceFile, diagnostics);

return { changesCount, diagnostics };
}
Expand DownExpand Up@@ -414,11 +414,17 @@ function migrateResourceCall(call: CallExpression, _sourceFile: SourceFile): boo

const TASK_OPTIONS = ['taskStore', 'taskMessageQueue'] as const;

function migrateConstructorTaskOptions(sourceFile: SourceFile, diagnostics: Diagnostic[]): number {
/**
* Flag v1 task runtime options on the McpServer constructor as removed.
*
* The experimental tasks runtime was removed in v2 (SEP-2663) with no replacement, so
* these options cannot be migrated automatically. Emit an action-required diagnostic
* matching the importMap removal entry for `experimental/tasks`; the source is left
* untouched.
*/
function flagRemovedTaskOptions(sourceFile: SourceFile, diagnostics: Diagnostic[]): void {
const localName = resolveLocalImportName(sourceFile, 'McpServer');
if (!localName) return 0;

let changes = 0;
if (!localName) return;

for (const node of sourceFile.getDescendantsOfKind(SyntaxKind.NewExpression)) {
if (node.wasForgotten()) continue;
Expand All@@ -431,110 +437,15 @@ function migrateConstructorTaskOptions(sourceFile: SourceFile, diagnostics: Diag
const optionsArg = args[1]!;
if (!Node.isObjectLiteralExpression(optionsArg)) continue;

// Check if any task options are present at the top level
const propsToMove: string[] = [];
for (const propName of TASK_OPTIONS) {
if (optionsArg.getProperty(propName)) {
propsToMove.push(propName);
}
}
if (propsToMove.length === 0) continue;

// Find the tasks object's position within the options text using AST,
// then do all mutations via a single text replacement to avoid node invalidation.
const capabilitiesProp = optionsArg.getProperty('capabilities');
let tasksObjStart = -1;
let tasksObjEnd = -1;
const optionsStart = optionsArg.getStart();
if (capabilitiesProp && Node.isPropertyAssignment(capabilitiesProp)) {
const capInit = capabilitiesProp.getInitializer();
if (capInit && Node.isObjectLiteralExpression(capInit)) {
const tasksProp = capInit.getProperty('tasks');
if (tasksProp && Node.isPropertyAssignment(tasksProp)) {
const tasksInit = tasksProp.getInitializer();
if (tasksInit && Node.isObjectLiteralExpression(tasksInit)) {
tasksObjStart = tasksInit.getStart() - optionsStart;
tasksObjEnd = tasksInit.getEnd() - optionsStart;
}
}
}
}

if (tasksObjStart === -1) {
for (const propName of propsToMove) {
diagnostics.push(
actionRequired(
sourceFile.getFilePath(),
node,
`Move '${propName}' from McpServer options into capabilities.tasks — v2 expects task runtime options inside the tasks capability.`
)
);
}
continue;
}

// Single text replacement: remove top-level props and insert into tasks object.
// Use AST nodes (already located via getProperty) to get brace-balanced text and
// exact positions, avoiding regex truncation on values containing commas/braces.
// Collect all properties first, then process in reverse position order so each
// removal doesn't invalidate the positions of subsequent removals.
let optionsText = optionsArg.getText();
const argStart = optionsArg.getStart();
const propsWithPositions: { text: string; start: number; end: number }[] = [];
for (const propName of propsToMove) {
const prop = optionsArg.getProperty(propName);
if (!prop) continue;
propsWithPositions.push({
text: prop.getText(),
start: prop.getStart() - argStart,
end: prop.getEnd() - argStart
});
if (!optionsArg.getProperty(propName)) continue;
diagnostics.push(
actionRequired(
sourceFile.getFilePath(),
node,
`Remove '${propName}' from McpServer options — experimental tasks removed in v2 (SEP-2663 — tasks moved to the Extensions Track). No v2 equivalent.`
)
);
}
const propTexts = propsWithPositions.map(p => p.text);

// Remove in reverse position order so earlier positions remain valid
const sortedProps = propsWithPositions.toSorted((a, b) => b.start - a.start);
for (const { start, end } of sortedProps) {
let remStart = start;
let remEnd = end;
// Consume trailing comma and whitespace
const afterProp = optionsText.slice(remEnd);
const trailingMatch = afterProp.match(/^\s*,?\s*/);
if (trailingMatch) {
remEnd += trailingMatch[0].length;
}
// Consume leading whitespace/newline
const beforeProp = optionsText.slice(0, remStart);
const leadingMatch = beforeProp.match(/[\n\r]?\s*$/);
if (leadingMatch) {
remStart -= leadingMatch[0].length;
}
optionsText = optionsText.slice(0, remStart) + optionsText.slice(remEnd);
// Adjust tasks position if removal was before it
if (remStart < tasksObjStart) {
const shift = remEnd - remStart;
tasksObjStart -= shift;
tasksObjEnd -= shift;
}
}

if (propTexts.length === 0) continue;

// Insert into the tasks object (just before its closing brace)
const tasksText = optionsText.slice(tasksObjStart, tasksObjEnd);
const closingBrace = tasksText.lastIndexOf('}');
const before = tasksText.slice(0, closingBrace).trimEnd();
const sep = before.length > 1 ? ',\n' : '\n';
const newTasksText = before + sep + propTexts.join(',\n') + '\n' + tasksText.slice(closingBrace);
optionsText = optionsText.slice(0, tasksObjStart) + newTasksText + optionsText.slice(tasksObjEnd);

// Clean up double/trailing commas
optionsText = optionsText.replaceAll(/,(\s*,)/g, ',');
optionsText = optionsText.replaceAll(/,(\s*})/g, '$1');

optionsArg.replaceWithText(optionsText);
changes += propTexts.length;
}

return changes;
}
59 changes: 59 additions & 0 deletions packages/codemod/test/v1-to-v2/transforms/mcpServerApi.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -307,4 +307,63 @@ describe('mcp-server-api transform', () => {
expect(result).toContain('registerTool("ping", {}');
expect(result).not.toContain('z.object');
});

it('flags taskStore in McpServer options as removed without modifying code', () => {
const input = [
`const server = new McpServer(`,
` { name: 'test', version: '1.0' },`,
` { taskStore: new InMemoryTaskStore() }`,
`);`,
''
].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input);
const taskDiags = result.diagnostics.filter(d => d.message.includes("'taskStore'"));
expect(taskDiags).toHaveLength(1);
expect(taskDiags[0]!.message).toContain('experimental tasks removed in v2 (SEP-2663');
expect(taskDiags[0]!.message).toContain('No v2 equivalent');
expect(taskDiags[0]!.insertComment).toBe(true);
});

it('flags each task option separately when both are present', () => {
const input = [
`const server = new McpServer(`,
` { name: 'test', version: '1.0' },`,
` { taskStore: store, taskMessageQueue: queue }`,
`);`,
''
].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input);
expect(result.diagnostics.some(d => d.message.includes("'taskStore'"))).toBe(true);
expect(result.diagnostics.some(d => d.message.includes("'taskMessageQueue'"))).toBe(true);
});

it('does not move task options into capabilities.tasks even when present', () => {
const input = [
`const server = new McpServer(`,
` { name: 'test', version: '1.0' },`,
` { taskStore: store, capabilities: { tasks: {} } }`,
`);`,
''
].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input);
expect(sourceFile.getFullText()).toContain('taskStore: store');
expect(result.diagnostics.some(d => d.message.includes("'taskStore'"))).toBe(true);
});

it('emits no task diagnostics for McpServer options without task options', () => {
const input = [`const server = new McpServer({ name: 'test', version: '1.0' }, { instructions: 'hi' });`, ''].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(result.diagnostics).toHaveLength(0);
});
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(codemod): flag removed task options instead of migrating them; drop stale task artifacts by felixweinberger · Pull Request #2288 · modelcontextprotocol/typescript-sdk · GitHub
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/codemod-flag-removed-task-options.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/codemod': patch
---

The v1→v2 codemod no longer rewrites `taskStore`/`taskMessageQueue` McpServer constructor options into `capabilities.tasks` — that target does not exist in v2 (the experimental tasks runtime was removed, SEP-2663). The codemod now leaves the code untouched and emits an action-required diagnostic telling migrators to remove the option, matching the removal guidance already given for `experimental/tasks` imports and the migration guide.
10 changes: 0 additions & 10 deletions .changeset/extract-task-manager.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/fix-task-session-isolation.md

This file was deleted.

127 changes: 19 additions & 108 deletions packages/codemod/src/migrations/v1-to-v2/transforms/mcpServerApi.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -121,7 +121,7 @@ export const mcpServerApiTransform: Transform = {
}
}

changesCount += migrateConstructorTaskOptions(sourceFile, diagnostics);
flagRemovedTaskOptions(sourceFile, diagnostics);

return { changesCount, diagnostics };
}
Expand DownExpand Up@@ -414,11 +414,17 @@ function migrateResourceCall(call: CallExpression, _sourceFile: SourceFile): boo

const TASK_OPTIONS = ['taskStore', 'taskMessageQueue'] as const;

function migrateConstructorTaskOptions(sourceFile: SourceFile, diagnostics: Diagnostic[]): number {
/**
* Flag v1 task runtime options on the McpServer constructor as removed.
*
* The experimental tasks runtime was removed in v2 (SEP-2663) with no replacement, so
* these options cannot be migrated automatically. Emit an action-required diagnostic
* matching the importMap removal entry for `experimental/tasks`; the source is left
* untouched.
*/
function flagRemovedTaskOptions(sourceFile: SourceFile, diagnostics: Diagnostic[]): void {
const localName = resolveLocalImportName(sourceFile, 'McpServer');
if (!localName) return 0;

let changes = 0;
if (!localName) return;

for (const node of sourceFile.getDescendantsOfKind(SyntaxKind.NewExpression)) {
if (node.wasForgotten()) continue;
Expand All@@ -431,110 +437,15 @@ function migrateConstructorTaskOptions(sourceFile: SourceFile, diagnostics: Diag
const optionsArg = args[1]!;
if (!Node.isObjectLiteralExpression(optionsArg)) continue;

// Check if any task options are present at the top level
const propsToMove: string[] = [];
for (const propName of TASK_OPTIONS) {
if (optionsArg.getProperty(propName)) {
propsToMove.push(propName);
}
}
if (propsToMove.length === 0) continue;

// Find the tasks object's position within the options text using AST,
// then do all mutations via a single text replacement to avoid node invalidation.
const capabilitiesProp = optionsArg.getProperty('capabilities');
let tasksObjStart = -1;
let tasksObjEnd = -1;
const optionsStart = optionsArg.getStart();
if (capabilitiesProp && Node.isPropertyAssignment(capabilitiesProp)) {
const capInit = capabilitiesProp.getInitializer();
if (capInit && Node.isObjectLiteralExpression(capInit)) {
const tasksProp = capInit.getProperty('tasks');
if (tasksProp && Node.isPropertyAssignment(tasksProp)) {
const tasksInit = tasksProp.getInitializer();
if (tasksInit && Node.isObjectLiteralExpression(tasksInit)) {
tasksObjStart = tasksInit.getStart() - optionsStart;
tasksObjEnd = tasksInit.getEnd() - optionsStart;
}
}
}
}

if (tasksObjStart === -1) {
for (const propName of propsToMove) {
diagnostics.push(
actionRequired(
sourceFile.getFilePath(),
node,
`Move '${propName}' from McpServer options into capabilities.tasks — v2 expects task runtime options inside the tasks capability.`
)
);
}
continue;
}

// Single text replacement: remove top-level props and insert into tasks object.
// Use AST nodes (already located via getProperty) to get brace-balanced text and
// exact positions, avoiding regex truncation on values containing commas/braces.
// Collect all properties first, then process in reverse position order so each
// removal doesn't invalidate the positions of subsequent removals.
let optionsText = optionsArg.getText();
const argStart = optionsArg.getStart();
const propsWithPositions: { text: string; start: number; end: number }[] = [];
for (const propName of propsToMove) {
const prop = optionsArg.getProperty(propName);
if (!prop) continue;
propsWithPositions.push({
text: prop.getText(),
start: prop.getStart() - argStart,
end: prop.getEnd() - argStart
});
if (!optionsArg.getProperty(propName)) continue;
diagnostics.push(
actionRequired(
sourceFile.getFilePath(),
node,
`Remove '${propName}' from McpServer options — experimental tasks removed in v2 (SEP-2663 — tasks moved to the Extensions Track). No v2 equivalent.`
)
);
}
const propTexts = propsWithPositions.map(p => p.text);

// Remove in reverse position order so earlier positions remain valid
const sortedProps = propsWithPositions.toSorted((a, b) => b.start - a.start);
for (const { start, end } of sortedProps) {
let remStart = start;
let remEnd = end;
// Consume trailing comma and whitespace
const afterProp = optionsText.slice(remEnd);
const trailingMatch = afterProp.match(/^\s*,?\s*/);
if (trailingMatch) {
remEnd += trailingMatch[0].length;
}
// Consume leading whitespace/newline
const beforeProp = optionsText.slice(0, remStart);
const leadingMatch = beforeProp.match(/[\n\r]?\s*$/);
if (leadingMatch) {
remStart -= leadingMatch[0].length;
}
optionsText = optionsText.slice(0, remStart) + optionsText.slice(remEnd);
// Adjust tasks position if removal was before it
if (remStart < tasksObjStart) {
const shift = remEnd - remStart;
tasksObjStart -= shift;
tasksObjEnd -= shift;
}
}

if (propTexts.length === 0) continue;

// Insert into the tasks object (just before its closing brace)
const tasksText = optionsText.slice(tasksObjStart, tasksObjEnd);
const closingBrace = tasksText.lastIndexOf('}');
const before = tasksText.slice(0, closingBrace).trimEnd();
const sep = before.length > 1 ? ',\n' : '\n';
const newTasksText = before + sep + propTexts.join(',\n') + '\n' + tasksText.slice(closingBrace);
optionsText = optionsText.slice(0, tasksObjStart) + newTasksText + optionsText.slice(tasksObjEnd);

// Clean up double/trailing commas
optionsText = optionsText.replaceAll(/,(\s*,)/g, ',');
optionsText = optionsText.replaceAll(/,(\s*})/g, '$1');

optionsArg.replaceWithText(optionsText);
changes += propTexts.length;
}

return changes;
}
59 changes: 59 additions & 0 deletions packages/codemod/test/v1-to-v2/transforms/mcpServerApi.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -307,4 +307,63 @@ describe('mcp-server-api transform', () => {
expect(result).toContain('registerTool("ping", {}');
expect(result).not.toContain('z.object');
});

it('flags taskStore in McpServer options as removed without modifying code', () => {
const input = [
`const server = new McpServer(`,
` { name: 'test', version: '1.0' },`,
` { taskStore: new InMemoryTaskStore() }`,
`);`,
''
].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input);
const taskDiags = result.diagnostics.filter(d => d.message.includes("'taskStore'"));
expect(taskDiags).toHaveLength(1);
expect(taskDiags[0]!.message).toContain('experimental tasks removed in v2 (SEP-2663');
expect(taskDiags[0]!.message).toContain('No v2 equivalent');
expect(taskDiags[0]!.insertComment).toBe(true);
});

it('flags each task option separately when both are present', () => {
const input = [
`const server = new McpServer(`,
` { name: 'test', version: '1.0' },`,
` { taskStore: store, taskMessageQueue: queue }`,
`);`,
''
].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input);
expect(result.diagnostics.some(d => d.message.includes("'taskStore'"))).toBe(true);
expect(result.diagnostics.some(d => d.message.includes("'taskMessageQueue'"))).toBe(true);
});

it('does not move task options into capabilities.tasks even when present', () => {
const input = [
`const server = new McpServer(`,
` { name: 'test', version: '1.0' },`,
` { taskStore: store, capabilities: { tasks: {} } }`,
`);`,
''
].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input);
expect(sourceFile.getFullText()).toContain('taskStore: store');
expect(result.diagnostics.some(d => d.message.includes("'taskStore'"))).toBe(true);
});

it('emits no task diagnostics for McpServer options without task options', () => {
const input = [`const server = new McpServer({ name: 'test', version: '1.0' }, { instructions: 'hi' });`, ''].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(result.diagnostics).toHaveLength(0);
});
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); fix(codemod): flag removed task options instead of migrating them; drop stale task artifacts by felixweinberger · Pull Request #2288 · modelcontextprotocol/typescript-sdk · GitHub
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/codemod-flag-removed-task-options.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/codemod': patch
---

The v1→v2 codemod no longer rewrites `taskStore`/`taskMessageQueue` McpServer constructor options into `capabilities.tasks` — that target does not exist in v2 (the experimental tasks runtime was removed, SEP-2663). The codemod now leaves the code untouched and emits an action-required diagnostic telling migrators to remove the option, matching the removal guidance already given for `experimental/tasks` imports and the migration guide.
10 changes: 0 additions & 10 deletions .changeset/extract-task-manager.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/fix-task-session-isolation.md

This file was deleted.

127 changes: 19 additions & 108 deletions packages/codemod/src/migrations/v1-to-v2/transforms/mcpServerApi.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -121,7 +121,7 @@ export const mcpServerApiTransform: Transform = {
}
}

changesCount += migrateConstructorTaskOptions(sourceFile, diagnostics);
flagRemovedTaskOptions(sourceFile, diagnostics);

return { changesCount, diagnostics };
}
Expand DownExpand Up@@ -414,11 +414,17 @@ function migrateResourceCall(call: CallExpression, _sourceFile: SourceFile): boo

const TASK_OPTIONS = ['taskStore', 'taskMessageQueue'] as const;

function migrateConstructorTaskOptions(sourceFile: SourceFile, diagnostics: Diagnostic[]): number {
/**
* Flag v1 task runtime options on the McpServer constructor as removed.
*
* The experimental tasks runtime was removed in v2 (SEP-2663) with no replacement, so
* these options cannot be migrated automatically. Emit an action-required diagnostic
* matching the importMap removal entry for `experimental/tasks`; the source is left
* untouched.
*/
function flagRemovedTaskOptions(sourceFile: SourceFile, diagnostics: Diagnostic[]): void {
const localName = resolveLocalImportName(sourceFile, 'McpServer');
if (!localName) return 0;

let changes = 0;
if (!localName) return;

for (const node of sourceFile.getDescendantsOfKind(SyntaxKind.NewExpression)) {
if (node.wasForgotten()) continue;
Expand All@@ -431,110 +437,15 @@ function migrateConstructorTaskOptions(sourceFile: SourceFile, diagnostics: Diag
const optionsArg = args[1]!;
if (!Node.isObjectLiteralExpression(optionsArg)) continue;

// Check if any task options are present at the top level
const propsToMove: string[] = [];
for (const propName of TASK_OPTIONS) {
if (optionsArg.getProperty(propName)) {
propsToMove.push(propName);
}
}
if (propsToMove.length === 0) continue;

// Find the tasks object's position within the options text using AST,
// then do all mutations via a single text replacement to avoid node invalidation.
const capabilitiesProp = optionsArg.getProperty('capabilities');
let tasksObjStart = -1;
let tasksObjEnd = -1;
const optionsStart = optionsArg.getStart();
if (capabilitiesProp && Node.isPropertyAssignment(capabilitiesProp)) {
const capInit = capabilitiesProp.getInitializer();
if (capInit && Node.isObjectLiteralExpression(capInit)) {
const tasksProp = capInit.getProperty('tasks');
if (tasksProp && Node.isPropertyAssignment(tasksProp)) {
const tasksInit = tasksProp.getInitializer();
if (tasksInit && Node.isObjectLiteralExpression(tasksInit)) {
tasksObjStart = tasksInit.getStart() - optionsStart;
tasksObjEnd = tasksInit.getEnd() - optionsStart;
}
}
}
}

if (tasksObjStart === -1) {
for (const propName of propsToMove) {
diagnostics.push(
actionRequired(
sourceFile.getFilePath(),
node,
`Move '${propName}' from McpServer options into capabilities.tasks — v2 expects task runtime options inside the tasks capability.`
)
);
}
continue;
}

// Single text replacement: remove top-level props and insert into tasks object.
// Use AST nodes (already located via getProperty) to get brace-balanced text and
// exact positions, avoiding regex truncation on values containing commas/braces.
// Collect all properties first, then process in reverse position order so each
// removal doesn't invalidate the positions of subsequent removals.
let optionsText = optionsArg.getText();
const argStart = optionsArg.getStart();
const propsWithPositions: { text: string; start: number; end: number }[] = [];
for (const propName of propsToMove) {
const prop = optionsArg.getProperty(propName);
if (!prop) continue;
propsWithPositions.push({
text: prop.getText(),
start: prop.getStart() - argStart,
end: prop.getEnd() - argStart
});
if (!optionsArg.getProperty(propName)) continue;
diagnostics.push(
actionRequired(
sourceFile.getFilePath(),
node,
`Remove '${propName}' from McpServer options — experimental tasks removed in v2 (SEP-2663 — tasks moved to the Extensions Track). No v2 equivalent.`
)
);
}
const propTexts = propsWithPositions.map(p => p.text);

// Remove in reverse position order so earlier positions remain valid
const sortedProps = propsWithPositions.toSorted((a, b) => b.start - a.start);
for (const { start, end } of sortedProps) {
let remStart = start;
let remEnd = end;
// Consume trailing comma and whitespace
const afterProp = optionsText.slice(remEnd);
const trailingMatch = afterProp.match(/^\s*,?\s*/);
if (trailingMatch) {
remEnd += trailingMatch[0].length;
}
// Consume leading whitespace/newline
const beforeProp = optionsText.slice(0, remStart);
const leadingMatch = beforeProp.match(/[\n\r]?\s*$/);
if (leadingMatch) {
remStart -= leadingMatch[0].length;
}
optionsText = optionsText.slice(0, remStart) + optionsText.slice(remEnd);
// Adjust tasks position if removal was before it
if (remStart < tasksObjStart) {
const shift = remEnd - remStart;
tasksObjStart -= shift;
tasksObjEnd -= shift;
}
}

if (propTexts.length === 0) continue;

// Insert into the tasks object (just before its closing brace)
const tasksText = optionsText.slice(tasksObjStart, tasksObjEnd);
const closingBrace = tasksText.lastIndexOf('}');
const before = tasksText.slice(0, closingBrace).trimEnd();
const sep = before.length > 1 ? ',\n' : '\n';
const newTasksText = before + sep + propTexts.join(',\n') + '\n' + tasksText.slice(closingBrace);
optionsText = optionsText.slice(0, tasksObjStart) + newTasksText + optionsText.slice(tasksObjEnd);

// Clean up double/trailing commas
optionsText = optionsText.replaceAll(/,(\s*,)/g, ',');
optionsText = optionsText.replaceAll(/,(\s*})/g, '$1');

optionsArg.replaceWithText(optionsText);
changes += propTexts.length;
}

return changes;
}
59 changes: 59 additions & 0 deletions packages/codemod/test/v1-to-v2/transforms/mcpServerApi.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -307,4 +307,63 @@ describe('mcp-server-api transform', () => {
expect(result).toContain('registerTool("ping", {}');
expect(result).not.toContain('z.object');
});

it('flags taskStore in McpServer options as removed without modifying code', () => {
const input = [
`const server = new McpServer(`,
` { name: 'test', version: '1.0' },`,
` { taskStore: new InMemoryTaskStore() }`,
`);`,
''
].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input);
const taskDiags = result.diagnostics.filter(d => d.message.includes("'taskStore'"));
expect(taskDiags).toHaveLength(1);
expect(taskDiags[0]!.message).toContain('experimental tasks removed in v2 (SEP-2663');
expect(taskDiags[0]!.message).toContain('No v2 equivalent');
expect(taskDiags[0]!.insertComment).toBe(true);
});

it('flags each task option separately when both are present', () => {
const input = [
`const server = new McpServer(`,
` { name: 'test', version: '1.0' },`,
` { taskStore: store, taskMessageQueue: queue }`,
`);`,
''
].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input);
expect(result.diagnostics.some(d => d.message.includes("'taskStore'"))).toBe(true);
expect(result.diagnostics.some(d => d.message.includes("'taskMessageQueue'"))).toBe(true);
});

it('does not move task options into capabilities.tasks even when present', () => {
const input = [
`const server = new McpServer(`,
` { name: 'test', version: '1.0' },`,
` { taskStore: store, capabilities: { tasks: {} } }`,
`);`,
''
].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(sourceFile.getFullText()).toBe(MCP_IMPORT + input);
expect(sourceFile.getFullText()).toContain('taskStore: store');
expect(result.diagnostics.some(d => d.message.includes("'taskStore'"))).toBe(true);
});

it('emits no task diagnostics for McpServer options without task options', () => {
const input = [`const server = new McpServer({ name: 'test', version: '1.0' }, { instructions: 'hi' });`, ''].join('\n');
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', MCP_IMPORT + input);
const result = mcpServerApiTransform.apply(sourceFile, ctx);
expect(result.diagnostics).toHaveLength(0);
});
});
Loading