Export Protocol and mergeCapabilities from client/server package roots (#2501)
This commit is contained in:
parent
e81758caed
commit
1480241e2a
12 changed files with 246 additions and 73 deletions
10
.changeset/export-protocol-class.md
Normal file
10
.changeset/export-protocol-class.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
'@modelcontextprotocol/core-internal': minor
|
||||
'@modelcontextprotocol/client': minor
|
||||
'@modelcontextprotocol/server': minor
|
||||
'@modelcontextprotocol/codemod': patch
|
||||
---
|
||||
|
||||
Export the `Protocol` base class and `mergeCapabilities` from the `@modelcontextprotocol/client` and `@modelcontextprotocol/server` package roots, restoring the v1 import for consumers that subclass `Protocol` (e.g. the MCP Apps SDK). The client and server packages each bundle their own compiled copy of the class, so import it from one package consistently within a process.
|
||||
|
||||
The codemod now rewrites `Protocol` and `mergeCapabilities` imports from `shared/protocol.js` to the client or server package root, like the module's other symbols, instead of dropping them with an action-required marker.
|
||||
|
|
@ -68,7 +68,7 @@ The SDK is organized into three main layers:
|
|||
The SDK separates internal code from the public API surface:
|
||||
|
||||
- **`@modelcontextprotocol/core-internal`** (main entry, `packages/core-internal/src/index.ts`) — Internal barrel. Exports everything (including Zod schemas, Protocol class, stdio utils). Only consumed by sibling packages within the monorepo (`private: true`).
|
||||
- **`@modelcontextprotocol/core-internal/public`** (`packages/core-internal/src/exports/public/index.ts`) — Curated public API. Exports only TypeScript types, error classes, constants, and guards. Re-exported by client and server packages.
|
||||
- **`@modelcontextprotocol/core-internal/public`** (`packages/core-internal/src/exports/public/index.ts`) — Curated public API. Exports TypeScript types, error classes, constants, guards, and the `Protocol` base class (+ `mergeCapabilities`). Re-exported by client and server packages.
|
||||
- **`@modelcontextprotocol/client`** and **`@modelcontextprotocol/server`** (`packages/*/src/index.ts`) — Final public surface. Package-specific exports (named explicitly) plus re-exports from `core-internal/public`.
|
||||
- **`@modelcontextprotocol/core`** (`packages/core/src/index.ts`) — Public Zod-schema package and the canonical home of the schema source modules (`src/schemas.ts`, `src/auth.ts`, `src/constants.ts`). The root entry re-exports **only** the `*Schema` Zod constants (MCP spec + OAuth/OpenID) — the published home for raw runtime validation (`CallToolResultSchema.parse(...)`); runtime-neutral (`zod` is its only dependency). The `./internal` subpath re-exports the schema modules wholesale for the sibling packages: `core-internal` re-exports them at the old module paths, and the `client`/`server`/`server-legacy` bundles resolve `@modelcontextprotocol/core/internal` as a real external dependency instead of carrying their own schema copies (their public surfaces stay Zod-free).
|
||||
|
||||
|
|
|
|||
|
|
@ -108,9 +108,8 @@ In addition the codemod:
|
|||
`completable(schema, cb).optional()` (see
|
||||
[Standard Schema objects](#standard-schema-objects-raw-shapes-deprecated)); shapes it
|
||||
cannot invert get an `@mcp-codemod-error` marker.
|
||||
- Drops `Protocol` / `mergeCapabilities` from `shared/protocol.js` imports, re-exports,
|
||||
mocks, and dynamic imports — no v2 package exports them — leaving a marker with the
|
||||
replacement at each site.
|
||||
- Rewrites `Protocol` / `mergeCapabilities` imports from `shared/protocol.js` to the
|
||||
client or server package root, like the module's other symbols.
|
||||
|
||||
## What the codemod does NOT handle
|
||||
|
||||
|
|
@ -1444,13 +1443,15 @@ now typed as JSON-compatible objects (nested JSON values) rather than arbitrary
|
|||
objects. A payload typed `Record<string, unknown>` no longer assigns (`TS2322`) — give
|
||||
the source a JSON-compatible type or cast at the boundary.
|
||||
|
||||
The `Protocol` base class itself is no longer exported (it is internal engine). If you
|
||||
were reaching into protocol internals — rare, mostly debugging tools —
|
||||
`client.fallbackRequestHandler` / `server.fallbackRequestHandler` receives every
|
||||
inbound request that no registered handler matches, before capability gating. Delete
|
||||
the v1 `shared/protocol.js` import: `Protocol` has no v2 import path. The codemod
|
||||
drops `Protocol` (and `mergeCapabilities`) from the rewritten import and leaves an
|
||||
`@mcp-codemod-error` marker at the site explaining the replacement.
|
||||
The `Protocol` base class and `mergeCapabilities` moved: import them from the
|
||||
`@modelcontextprotocol/client` or `@modelcontextprotocol/server` package root instead
|
||||
of `shared/protocol.js`. Most code should not use `Protocol` directly — `Client` and
|
||||
`Server` are the supported surfaces, and for observing unmatched inbound requests
|
||||
prefer `client.fallbackRequestHandler` / `server.fallbackRequestHandler`. The codemod
|
||||
rewrites `Protocol` and `mergeCapabilities` imports to the package root, like the
|
||||
module's other symbols. One caveat: the client and server packages each bundle their
|
||||
own compiled copy of the class, so the two roots' `Protocol` exports are distinct
|
||||
classes — import it from one package consistently within a process.
|
||||
|
||||
#### JSON Schema 2020-12 posture (SEP-1613, SEP-2106)
|
||||
|
||||
|
|
|
|||
14
packages/client/test/client/protocolExport.test.ts
Normal file
14
packages/client/test/client/protocolExport.test.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* Pins that `Protocol` and `mergeCapabilities` are exported from the package
|
||||
* root (carried by the `export *` of the core-internal public barrel).
|
||||
*/
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { mergeCapabilities, Protocol } from '../../src/index';
|
||||
|
||||
describe('package root exports', () => {
|
||||
test('Protocol and mergeCapabilities are exported from the client root', () => {
|
||||
expect(typeof Protocol).toBe('function');
|
||||
expect(typeof mergeCapabilities).toBe('function');
|
||||
});
|
||||
});
|
||||
|
|
@ -42,8 +42,8 @@ guards that mix the two enums), add `import { z } from 'zod'` when a wrap needs
|
|||
it, rewrite `vi.mock`
|
||||
/ `jest.mock` / dynamic `import()` paths, invert optional completable nesting
|
||||
(`completable(schema.optional(), cb)` becomes `completable(schema, cb).optional()`),
|
||||
and drop `Protocol` / `mergeCapabilities` (no v2 export) with an action-required
|
||||
marker naming the replacement.
|
||||
and rewrite `Protocol` / `mergeCapabilities` imports from `shared/protocol.js` to the
|
||||
client or server package root.
|
||||
|
||||
## `@mcp-codemod-error` markers
|
||||
|
||||
|
|
|
|||
|
|
@ -173,17 +173,7 @@ export const IMPORT_MAP: Record<string, ImportMapping> = {
|
|||
},
|
||||
'@modelcontextprotocol/sdk/shared/protocol.js': {
|
||||
target: 'RESOLVE_BY_CONTEXT',
|
||||
status: 'moved',
|
||||
removedSymbols: {
|
||||
Protocol:
|
||||
'The Protocol base class is not exported by the v2 packages. To observe or handle inbound requests ' +
|
||||
'that have no registered handler, use client.fallbackRequestHandler / server.fallbackRequestHandler; ' +
|
||||
'build custom behavior on Client or Server instead of subclassing Protocol. ' +
|
||||
'See the migration guide: Behavioral changes > Client connection & dispatch.',
|
||||
mergeCapabilities:
|
||||
'mergeCapabilities() is not exported by the v2 packages. Pass the complete capabilities object to the ' +
|
||||
'Client/Server constructor, or merge capability objects with a plain object spread.'
|
||||
}
|
||||
status: 'moved'
|
||||
},
|
||||
'@modelcontextprotocol/sdk/shared/transport.js': {
|
||||
target: 'RESOLVE_BY_CONTEXT',
|
||||
|
|
|
|||
|
|
@ -225,9 +225,9 @@ export const importPathsTransform: Transform = {
|
|||
);
|
||||
}
|
||||
}
|
||||
// Qualified accesses to symbols with no v2 export (`ns.Protocol`) can't be fixed by
|
||||
// Qualified accesses to symbols with no v2 export (`ns.GoneClass`) can't be fixed by
|
||||
// moving the namespace binding — flag each accessed one. Expression positions are
|
||||
// PropertyAccessExpressions; type positions (`let p: ns.Protocol`) are QualifiedNames.
|
||||
// PropertyAccessExpressions; type positions (`let p: ns.GoneClass`) are QualifiedNames.
|
||||
if (mapping.removedSymbols) {
|
||||
const nsName = namespaceImport.getText();
|
||||
const accessedRemoved = new Map<string, Node>();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { Project } from 'ts-morph';
|
||||
|
||||
import { IMPORT_MAP } from '../../../src/migrations/v1-to-v2/mappings/importMap';
|
||||
import { importPathsTransform } from '../../../src/migrations/v1-to-v2/transforms/importPaths';
|
||||
import type { TransformContext } from '../../../src/types';
|
||||
|
||||
|
|
@ -11,6 +12,13 @@ function applyTransform(code: string, context: TransformContext = { projectType:
|
|||
return sourceFile.getFullText();
|
||||
}
|
||||
|
||||
function applyWithDiagnostics(code: string, context: TransformContext = { projectType: 'server' }) {
|
||||
const project = new Project({ useInMemoryFileSystem: true });
|
||||
const sourceFile = project.createSourceFile('test.ts', code);
|
||||
const result = importPathsTransform.apply(sourceFile, context);
|
||||
return { text: sourceFile.getFullText(), result };
|
||||
}
|
||||
|
||||
describe('import-paths transform', () => {
|
||||
it('rewrites client imports to @modelcontextprotocol/client', () => {
|
||||
const input = `import { Client } from '@modelcontextprotocol/sdk/client/index.js';\n`;
|
||||
|
|
@ -1006,93 +1014,139 @@ describe('auth types routing (B3)', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('symbols with no v2 export (removedSymbols)', () => {
|
||||
function applyWithDiagnostics(code: string, context: TransformContext = { projectType: 'server' }) {
|
||||
const project = new Project({ useInMemoryFileSystem: true });
|
||||
const sourceFile = project.createSourceFile('test.ts', code);
|
||||
const result = importPathsTransform.apply(sourceFile, context);
|
||||
return { text: sourceFile.getFullText(), result };
|
||||
}
|
||||
|
||||
it('drops Protocol from a shared/protocol.js import and flags it', () => {
|
||||
describe('Protocol and mergeCapabilities route like other shared/protocol.js symbols', () => {
|
||||
it('rewrites a Protocol import to the context package root', () => {
|
||||
const input = `import { Protocol } from '@modelcontextprotocol/sdk/shared/protocol.js';\n`;
|
||||
const { text, result } = applyWithDiagnostics(input);
|
||||
expect(text).not.toContain('Protocol }');
|
||||
expect(text).toMatch(/import \{ Protocol \} from ['"]@modelcontextprotocol\/server['"]/);
|
||||
expect(text).not.toContain('@modelcontextprotocol/sdk');
|
||||
const diag = result.diagnostics.find(d => d.insertComment && d.message.includes('Protocol base class'));
|
||||
expect(result.diagnostics.filter(d => d.insertComment)).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps Protocol alongside its siblings in a mixed import', () => {
|
||||
const input = `import { Protocol, type ProtocolOptions } from '@modelcontextprotocol/sdk/shared/protocol.js';\n`;
|
||||
const { text, result } = applyWithDiagnostics(input);
|
||||
expect(text).toContain('Protocol');
|
||||
expect(text).toContain('ProtocolOptions');
|
||||
expect(text).toContain('@modelcontextprotocol/server');
|
||||
expect(result.diagnostics.filter(d => d.insertComment)).toEqual([]);
|
||||
});
|
||||
|
||||
it('rewrites a mergeCapabilities import with no spread guidance', () => {
|
||||
const input = `import { mergeCapabilities } from '@modelcontextprotocol/sdk/shared/protocol.js';\n`;
|
||||
const { text, result } = applyWithDiagnostics(input);
|
||||
expect(text).toMatch(/import \{ mergeCapabilities \} from ['"]@modelcontextprotocol\/server['"]/);
|
||||
expect(result.diagnostics.some(d => d.message.includes('object spread'))).toBe(false);
|
||||
});
|
||||
|
||||
it('rewrites a named re-export of Protocol', () => {
|
||||
const input = `export { Protocol } from '@modelcontextprotocol/sdk/shared/protocol.js';\n`;
|
||||
const { text, result } = applyWithDiagnostics(input);
|
||||
expect(text).toMatch(/export \{ Protocol \} from ['"]@modelcontextprotocol\/server['"]/);
|
||||
expect(result.diagnostics.some(d => d.message.includes('no v2 export'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('symbols with no v2 export (removedSymbols)', () => {
|
||||
// No live mapping uses removedSymbols today (Protocol/mergeCapabilities were the
|
||||
// last, until they became public exports). The machinery stays for future
|
||||
// removals; these tests pin it against a synthetic mapping.
|
||||
const SYNTHETIC = '@modelcontextprotocol/sdk/shared/synthetic.js';
|
||||
|
||||
beforeAll(() => {
|
||||
IMPORT_MAP[SYNTHETIC] = {
|
||||
target: 'RESOLVE_BY_CONTEXT',
|
||||
status: 'moved',
|
||||
removedSymbols: {
|
||||
GoneClass: 'The GoneClass base class is not exported by the v2 packages. ' + 'See the migration guide for the replacement.',
|
||||
goneHelper: 'goneHelper() is not exported by the v2 packages. Use a plain object spread.'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
delete IMPORT_MAP[SYNTHETIC];
|
||||
});
|
||||
|
||||
it('drops a removed symbol from an import and flags it', () => {
|
||||
const input = `import { GoneClass } from '${SYNTHETIC}';\n`;
|
||||
const { text, result } = applyWithDiagnostics(input);
|
||||
expect(text).not.toContain('GoneClass }');
|
||||
expect(text).not.toContain('@modelcontextprotocol/sdk');
|
||||
const diag = result.diagnostics.find(d => d.insertComment && d.message.includes('GoneClass base class'));
|
||||
expect(diag).toBeDefined();
|
||||
expect(diag?.message).toContain('fallbackRequestHandler');
|
||||
expect(diag?.message).toContain('migration guide');
|
||||
});
|
||||
|
||||
it('routes surviving siblings while dropping the removed symbol', () => {
|
||||
const input = `import { Protocol, type ProtocolOptions } from '@modelcontextprotocol/sdk/shared/protocol.js';\n`;
|
||||
const input = `import { GoneClass, type Survivor } from '${SYNTHETIC}';\n`;
|
||||
const { text, result } = applyWithDiagnostics(input);
|
||||
expect(text).toContain('ProtocolOptions');
|
||||
expect(text).toContain('Survivor');
|
||||
expect(text).toContain('@modelcontextprotocol/server');
|
||||
expect(text).not.toMatch(/\bProtocol\b(?!Options)/);
|
||||
expect(result.diagnostics.some(d => d.message.includes('Protocol base class'))).toBe(true);
|
||||
expect(text).not.toMatch(/\bGoneClass\b/);
|
||||
expect(result.diagnostics.some(d => d.message.includes('GoneClass base class'))).toBe(true);
|
||||
});
|
||||
|
||||
it('flags mergeCapabilities with spread guidance', () => {
|
||||
const input = `import { mergeCapabilities } from '@modelcontextprotocol/sdk/shared/protocol.js';\n`;
|
||||
it('flags a removed helper with its configured guidance', () => {
|
||||
const input = `import { goneHelper } from '${SYNTHETIC}';\n`;
|
||||
const { result } = applyWithDiagnostics(input);
|
||||
const diag = result.diagnostics.find(d => d.message.includes('mergeCapabilities'));
|
||||
const diag = result.diagnostics.find(d => d.message.includes('goneHelper'));
|
||||
expect(diag).toBeDefined();
|
||||
expect(diag?.message).toContain('object spread');
|
||||
});
|
||||
|
||||
it('does not resolve a context package for an import of only removed symbols', () => {
|
||||
const input = `import { Protocol } from '@modelcontextprotocol/sdk/shared/protocol.js';\n`;
|
||||
const input = `import { GoneClass } from '${SYNTHETIC}';\n`;
|
||||
const { result } = applyWithDiagnostics(input, { projectType: 'unknown' });
|
||||
// projectType 'unknown' would emit a could-not-determine warning if context were resolved.
|
||||
expect(result.diagnostics.some(d => d.message.includes('could not determine'))).toBe(false);
|
||||
});
|
||||
|
||||
it('flags qualified accesses to removed symbols on a namespace import', () => {
|
||||
const input = `import * as proto from '@modelcontextprotocol/sdk/shared/protocol.js';\nclass Mine extends proto.Protocol {}\n`;
|
||||
const input = `import * as synth from '${SYNTHETIC}';\nclass Mine extends synth.GoneClass {}\n`;
|
||||
const { text, result } = applyWithDiagnostics(input);
|
||||
expect(text).toContain('@modelcontextprotocol/server');
|
||||
expect(result.diagnostics.some(d => d.insertComment && d.message.includes('Protocol base class'))).toBe(true);
|
||||
expect(result.diagnostics.some(d => d.insertComment && d.message.includes('GoneClass base class'))).toBe(true);
|
||||
});
|
||||
|
||||
it('warns on a named re-export of Protocol with module-scoped guidance', () => {
|
||||
const input = `export { Protocol } from '@modelcontextprotocol/sdk/shared/protocol.js';\n`;
|
||||
it('warns on a named re-export of a removed symbol with module-scoped guidance', () => {
|
||||
const input = `export { GoneClass } from '${SYNTHETIC}';\n`;
|
||||
const { result } = applyWithDiagnostics(input);
|
||||
const diag = result.diagnostics.find(d => d.message.includes('Re-exported Protocol has no v2 export'));
|
||||
const diag = result.diagnostics.find(d => d.message.includes('Re-exported GoneClass has no v2 export'));
|
||||
expect(diag).toBeDefined();
|
||||
expect(diag?.message).toContain('fallbackRequestHandler');
|
||||
expect(diag?.message).toContain('migration guide');
|
||||
});
|
||||
|
||||
it('flags a star re-export of a module with removed symbols', () => {
|
||||
const input = `export * from '@modelcontextprotocol/sdk/shared/protocol.js';\n`;
|
||||
const input = `export * from '${SYNTHETIC}';\n`;
|
||||
const { result } = applyWithDiagnostics(input);
|
||||
const messages = result.diagnostics.map(d => d.message);
|
||||
expect(messages.some(m => m.includes('Star re-export') && m.includes('Protocol'))).toBe(true);
|
||||
expect(messages.some(m => m.includes('Star re-export') && m.includes('mergeCapabilities'))).toBe(true);
|
||||
expect(messages.some(m => m.includes('Star re-export') && m.includes('GoneClass'))).toBe(true);
|
||||
expect(messages.some(m => m.includes('Star re-export') && m.includes('goneHelper'))).toBe(true);
|
||||
});
|
||||
|
||||
it('flags type-position namespace accesses (QualifiedName) to removed symbols', () => {
|
||||
const input = `import * as proto from '@modelcontextprotocol/sdk/shared/protocol.js';\nexport function f(p: proto.Protocol): void {}\n`;
|
||||
const input = `import * as synth from '${SYNTHETIC}';\nexport function f(p: synth.GoneClass): void {}\n`;
|
||||
const { text, result } = applyWithDiagnostics(input);
|
||||
expect(text).toContain('@modelcontextprotocol/server');
|
||||
expect(result.diagnostics.some(d => d.insertComment && d.message.includes('Protocol base class'))).toBe(true);
|
||||
expect(result.diagnostics.some(d => d.insertComment && d.message.includes('GoneClass base class'))).toBe(true);
|
||||
});
|
||||
|
||||
it('anchors the dropped-symbol marker to a usage site that survives the rewrite', () => {
|
||||
const project = new Project({ useInMemoryFileSystem: true });
|
||||
const sourceFile = project.createSourceFile(
|
||||
'test.ts',
|
||||
`import { Protocol } from '@modelcontextprotocol/sdk/shared/protocol.js';\n\nexport class Mine extends Protocol {}\n`
|
||||
`import { GoneClass } from '${SYNTHETIC}';\n\nexport class Mine extends GoneClass {}\n`
|
||||
);
|
||||
const result = importPathsTransform.apply(sourceFile, { projectType: 'server' });
|
||||
const diag = result.diagnostics.find(d => d.message.includes('Protocol base class'));
|
||||
const diag = result.diagnostics.find(d => d.message.includes('GoneClass base class'));
|
||||
expect(diag).toBeDefined();
|
||||
// resolveCurrentLine resolves against the live usage node, not the removed import.
|
||||
const finalLine =
|
||||
sourceFile
|
||||
.getFullText()
|
||||
.split('\n')
|
||||
.findIndex(l => l.includes('extends Protocol')) + 1;
|
||||
.findIndex(l => l.includes('extends GoneClass')) + 1;
|
||||
expect(diag?.resolveCurrentLine?.()).toBe(finalLine);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { Project } from 'ts-morph';
|
||||
|
||||
import { IMPORT_MAP } from '../../../src/migrations/v1-to-v2/mappings/importMap';
|
||||
import { mockPathsTransform } from '../../../src/migrations/v1-to-v2/transforms/mockPaths';
|
||||
import type { TransformContext } from '../../../src/types';
|
||||
|
||||
|
|
@ -578,19 +579,49 @@ describe('removed symbols in mocks and dynamic imports', () => {
|
|||
return { text: sourceFile.getFullText(), result };
|
||||
}
|
||||
|
||||
it('leaves a mock factory providing Protocol unrewritten and flags it', () => {
|
||||
const input = `vi.mock('@modelcontextprotocol/sdk/shared/protocol.js', () => ({ Protocol: class {} }));\n`;
|
||||
const SYNTHETIC = '@modelcontextprotocol/sdk/shared/synthetic.js';
|
||||
|
||||
beforeAll(() => {
|
||||
IMPORT_MAP[SYNTHETIC] = {
|
||||
target: 'RESOLVE_BY_CONTEXT',
|
||||
status: 'moved',
|
||||
removedSymbols: {
|
||||
GoneClass: 'The GoneClass base class is not exported by the v2 packages.'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
delete IMPORT_MAP[SYNTHETIC];
|
||||
});
|
||||
|
||||
it('rewrites a mock factory providing Protocol like other surviving symbols', () => {
|
||||
const input = `vi.mock('@modelcontextprotocol/sdk/shared/protocol.js', () => ({ Protocol: class {} }));\nimport '@modelcontextprotocol/sdk/server/mcp.js';\n`;
|
||||
const { text, result } = applyWithDiagnostics(input);
|
||||
expect(text).toContain('@modelcontextprotocol/sdk/shared/protocol.js');
|
||||
expect(text).toContain(`vi.mock('@modelcontextprotocol/server'`);
|
||||
expect(result.diagnostics.filter(d => d.insertComment)).toEqual([]);
|
||||
});
|
||||
|
||||
it('rewrites a dynamic import destructuring Protocol', () => {
|
||||
const input = `const { Protocol } = await import('@modelcontextprotocol/sdk/shared/protocol.js');\nexport { Protocol };\n`;
|
||||
const { text, result } = applyWithDiagnostics(input);
|
||||
expect(text).toContain(`await import('@modelcontextprotocol/server')`);
|
||||
expect(result.diagnostics.filter(d => d.insertComment)).toEqual([]);
|
||||
});
|
||||
|
||||
it('leaves a mock factory providing a removed symbol unrewritten and flags it', () => {
|
||||
const input = `vi.mock('${SYNTHETIC}', () => ({ GoneClass: class {} }));\n`;
|
||||
const { text, result } = applyWithDiagnostics(input);
|
||||
expect(text).toContain(SYNTHETIC);
|
||||
const diag = result.diagnostics.find(d => d.insertComment);
|
||||
expect(diag?.message).toContain('Protocol');
|
||||
expect(diag?.message).toContain('GoneClass');
|
||||
expect(diag?.message).toContain('no v2 package exports');
|
||||
});
|
||||
|
||||
it('leaves a dynamic import destructuring Protocol unrewritten and flags it', () => {
|
||||
const input = `const { Protocol } = await import('@modelcontextprotocol/sdk/shared/protocol.js');\nexport { Protocol };\n`;
|
||||
it('leaves a dynamic import destructuring a removed symbol unrewritten and flags it', () => {
|
||||
const input = `const { GoneClass } = await import('${SYNTHETIC}');\nexport { GoneClass };\n`;
|
||||
const { text, result } = applyWithDiagnostics(input);
|
||||
expect(text).toContain('@modelcontextprotocol/sdk/shared/protocol.js');
|
||||
expect(text).toContain(SYNTHETIC);
|
||||
const diag = result.diagnostics.find(d => d.insertComment);
|
||||
expect(diag?.message).toContain('undefined at runtime');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
* This module defines the stable, public-facing API surface. Client and server
|
||||
* packages re-export from here so that end users only see supported symbols.
|
||||
*
|
||||
* Internal utilities (Protocol class, stdio parsing, schema helpers, etc.)
|
||||
* remain available via the internal barrel (@modelcontextprotocol/core-internal) for
|
||||
* use by client/server packages.
|
||||
* Internal utilities (stdio parsing, schema helpers, etc.) remain available via the
|
||||
* internal barrel (@modelcontextprotocol/core-internal) for use by client/server
|
||||
* packages.
|
||||
*/
|
||||
|
||||
// Auth error classes
|
||||
|
|
@ -45,7 +45,7 @@ export { isJsonContentType } from '../../shared/mediaType';
|
|||
// Metadata utilities
|
||||
export { getDisplayName } from '../../shared/metadataUtils';
|
||||
|
||||
// Protocol types (NOT the Protocol class itself or mergeCapabilities)
|
||||
// Protocol types and the Protocol base class
|
||||
export type {
|
||||
BaseContext,
|
||||
ClientContext,
|
||||
|
|
@ -57,7 +57,7 @@ export type {
|
|||
RequestStateAccessor,
|
||||
ServerContext
|
||||
} from '../../shared/protocol';
|
||||
export { DEFAULT_REQUEST_TIMEOUT_MSEC } from '../../shared/protocol';
|
||||
export { DEFAULT_REQUEST_TIMEOUT_MSEC, mergeCapabilities, Protocol } from '../../shared/protocol';
|
||||
|
||||
// stdio message framing utilities (for custom transport authors)
|
||||
export { deserializeMessage, ReadBuffer, serializeMessage, STDIO_DEFAULT_MAX_BUFFER_SIZE } from '../../shared/stdio';
|
||||
|
|
|
|||
59
packages/core-internal/test/shared/publicProtocol.test.ts
Normal file
59
packages/core-internal/test/shared/publicProtocol.test.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* Pins the public export of the `Protocol` base class and `mergeCapabilities`
|
||||
* (consumed by subclassing SDKs such as ext-apps), and that a bare `Protocol`
|
||||
* subclass pair exchanges custom methods with no MCP handshake on the wire.
|
||||
*/
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import * as z from 'zod/v4';
|
||||
|
||||
import type { BaseContext } from '../../src/exports/public/index';
|
||||
import { InMemoryTransport, mergeCapabilities, Protocol } from '../../src/exports/public/index';
|
||||
import type { JSONRPCMessage } from '../../src/types/types';
|
||||
|
||||
class TestProtocol extends Protocol<BaseContext> {
|
||||
protected buildContext(ctx: BaseContext): BaseContext {
|
||||
return ctx;
|
||||
}
|
||||
protected assertCapabilityForMethod(): void {}
|
||||
protected assertNotificationCapability(): void {}
|
||||
protected assertRequestHandlerCapability(): void {}
|
||||
}
|
||||
|
||||
describe('public Protocol class', () => {
|
||||
test('Protocol and mergeCapabilities are exported from the public barrel', () => {
|
||||
expect(typeof Protocol).toBe('function');
|
||||
expect(typeof mergeCapabilities).toBe('function');
|
||||
});
|
||||
|
||||
test('a Protocol subclass exchanges custom methods with no initialize on the wire', async () => {
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
// Capture BOTH directions: the accepting side must be as silent as the
|
||||
// requesting side — deployed peers understand nothing but the custom
|
||||
// methods.
|
||||
const sentByA: string[] = [];
|
||||
const sentByB: string[] = [];
|
||||
const captureSends = (transport: InMemoryTransport, into: string[]) => {
|
||||
const originalSend = transport.send.bind(transport);
|
||||
transport.send = async (message: JSONRPCMessage, options) => {
|
||||
if ('method' in message) into.push(message.method);
|
||||
return originalSend(message, options);
|
||||
};
|
||||
};
|
||||
captureSends(clientTransport, sentByA);
|
||||
captureSends(serverTransport, sentByB);
|
||||
|
||||
const a = new TestProtocol();
|
||||
const b = new TestProtocol();
|
||||
b.setRequestHandler('acme/echo', { params: z.object({ value: z.string() }) }, params => ({
|
||||
echoed: params.value
|
||||
}));
|
||||
|
||||
await b.connect(serverTransport);
|
||||
await a.connect(clientTransport);
|
||||
const result = await a.request({ method: 'acme/echo', params: { value: 'hi' } }, z.object({ echoed: z.string() }));
|
||||
|
||||
expect(result).toEqual({ echoed: 'hi' });
|
||||
expect(sentByA).toEqual(['acme/echo']);
|
||||
expect(sentByB).toEqual([]);
|
||||
});
|
||||
});
|
||||
14
packages/server/test/server/protocolExport.test.ts
Normal file
14
packages/server/test/server/protocolExport.test.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* Pins that `Protocol` and `mergeCapabilities` are exported from the package
|
||||
* root (carried by the `export *` of the core-internal public barrel).
|
||||
*/
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { mergeCapabilities, Protocol } from '../../src/index';
|
||||
|
||||
describe('package root exports', () => {
|
||||
test('Protocol and mergeCapabilities are exported from the server root', () => {
|
||||
expect(typeof Protocol).toBe('function');
|
||||
expect(typeof mergeCapabilities).toBe('function');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue