Export Protocol and mergeCapabilities from client/server package roots (#2501)

This commit is contained in:
Felix Weinberger 2026-07-17 14:37:31 +01:00 • committed by GitHub
commit 1480241e2a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 246 additions and 73 deletions

View 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.

View file

@ -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).

View file

@ -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)

View 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');
});
});

View file

@ -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

View file

@ -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',

View file

@ -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>();

View file

@ -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);
});
});

View file

@ -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');
});

View file

@ -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';

View 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([]);
});
});

View 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');
});
});