Skip to main content

Migrating from 2.x to 3.0.0

3.0.0 is not released yet. Everything below is already deprecated in 2.2.0 and still works through 2.x. You can make these changes now, on 2.2 or later, and 3.0 will need nothing more from you. This guide grows as 3.0 takes shape.

2.2.0 added the artifact bundle: the build writes everything the server needs to one file, build/mcp/bundle.json, and the server takes that one value. The older ways of passing the pieces separately are removed in 3.0. See ADR-0001 for why.

If you're an agent, go to For agents. It is a checklist you can run top to bottom.

1. Pass the artifact bundle to the web handler​

Import bundle.json instead of docs.json, search-index.json, and skills.json. name, version, and baseUrl now default to what the site was built with (the plugin's server.name, server.version, and the site URL), so you can drop them unless you want to override them.

import { createWebRequestHandler } from 'docusaurus-plugin-mcp-server/adapters';
-import docs from '../build/mcp/docs.json';
-import searchIndex from '../build/mcp/search-index.json';
-import skills from '../build/mcp/skills.json';
+import bundle from '../build/mcp/bundle.json';

export default {
fetch: createWebRequestHandler({
- docs,
- searchIndexData: searchIndex,
- skills,
- name: 'my-docs',
- baseUrl: 'https://docs.example.com',
+ artifacts: bundle,
}),
};

The same applies to new McpDocsServer({ docs, searchIndexData, skills, ... }): use new McpDocsServer({ artifacts: bundle }).

2. Point the Node server at the build directory​

createNodeServer({
- docsPath: './build/mcp/docs.json',
- indexPath: './build/mcp/search-index.json',
- skillsPath: './build/mcp/skills.json',
- name: 'my-docs',
+ artifactsDir: './build/mcp',
});

For new McpDocsServer({ docsPath, indexPath, skillsPath, ... }), read the bundle first:

+import { readArtifactBundle } from 'docusaurus-plugin-mcp-server/adapters/node';
+
-const server = new McpDocsServer({
- docsPath: './build/mcp/docs.json',
- indexPath: './build/mcp/search-index.json',
- name: 'my-docs',
-});
+const server = new McpDocsServer({ artifacts: await readArtifactBundle('./build/mcp') });

3. Read the bundle in custom search providers​

With an artifacts or artifactsDir server config (steps 1 and 2), SearchProvider.initialize receives initData.bundle. The docsPath, indexPath, docs, and indexData fields are removed in 3.0.

async initialize(context, initData) {
- const docs = initData.docs ?? JSON.parse(await readFile(initData.docsPath, 'utf8'));
+ // McpDocsServer sets bundle for artifacts/artifactsDir configs (steps 1 and 2).
+ if (!initData?.bundle) throw new Error('Pass { bundle } to initialize()');
+ const docs = initData.bundle.docs;
+ const index = initData.bundle.searchIndex; // if an indexer produced one
+ const mine = initData.bundle.extras?.['my-index.json']; // what your indexer's finalize() returned
}

If your code drives a provider itself (for example with evaluateSearch), pass the bundle instead of paths or pre-loaded data:

+import { readArtifactBundle } from 'docusaurus-plugin-mcp-server/adapters/node';
+
await provider.initialize(context, {
- docsPath: 'build/mcp/docs.json',
- indexPath: 'build/mcp/search-index.json',
+ bundle: await readArtifactBundle('build/mcp'),
});

4. Rename the deprecated config types​

The 2.1 config types keep their 2.1 shapes through 2.x and are removed in 3.0. Their replacements accept both the new and the old configs:

2.1 type (deprecated)Use instead
McpServerConfigMcpDocsServerConfig (any config), or McpServerBundleConfig
McpServerFileConfig, McpServerDataConfigMcpServerBundleConfig
WebRequestAdapterConfigWebRequestHandlerConfig
NodeServerOptionsNodeAdapterOptions

In the new types, name is optional (it defaults to the build's), so code that reads config.name should handle undefined.

getDocument and getDocCount are optional since 2.2: without them the server answers docs_fetch and the status document count from the bundle.

5. Only bundle.json is written​

Through 2.x the build also writes docs.json, search-index.json, skills.json, manifest.json, and each indexer extra as separate files. In 3.0 it writes only bundle.json. If anything outside this package reads those files (deploy scripts, a CDN rule, your own tooling), read bundle.json instead, or use readArtifactBundle(dir).

LocalSearchIndexer.finalize() stops returning docs.json; the plugin writes documents itself.

6. Serve Node requests with createNodeHandler​

McpDocsServer.handleHttpRequest(req, res, parsedBody?) is removed in 3.0. createNodeHandler does the same bridging and also answers the status check, the CORS preflight, and other methods, the same way the web handler does. It uses req.body when a body parser has already read the request, so it works behind express.json():

-const server = new McpDocsServer({ artifacts });
-app.post('/mcp', express.json(), (req, res) => server.handleHttpRequest(req, res, req.body));
+import { createNodeHandler } from 'docusaurus-plugin-mcp-server/adapters/node';
+
+app.all('/mcp', express.json(), createNodeHandler({ artifacts }));

To keep sending only POST to it, mount it with app.post instead. createNodeHandler sends CORS headers allowing every origin (Access-Control-Allow-Origin: *) by default, which handleHttpRequest never did. Pass corsOrigin: 'https://your.site' to restrict them, or corsOrigin: false to send none (for example, if you set your own).

7. Search providers: SearchRanker, isReady, healthCheck, and the plugin search option​

Since 2.2, the server needs only a name and a search function from a search provider (the SearchRanker type); initialize, getDocument, and getDocCount are optional. In 3.0:

  • SearchProvider becomes SearchRanker's shape: initialize is optional, and isReady and healthCheck are removed.
  • The server stops calling isReady(). Through 2.x, when it returns false the tools answer "Server not initialized". To fail a provider that can't work, reject from initialize() (the server then fails to initialize, and requests and the GET status return an error) or throw from search() (that docs_search call gets an error result).
  • healthCheck() was never called by the server. Use the GET status endpoint or McpDocsServer.getStatus().
  • The plugin's search option is removed. It has done nothing since 2.0: the provider is chosen in the server config.
-import type { SearchProvider } from 'docusaurus-plugin-mcp-server';
+import type { SearchRanker } from 'docusaurus-plugin-mcp-server';
import type {
ProviderContext,
SearchOptions,
SearchProviderInitData,
SearchResult,
} from 'docusaurus-plugin-mcp-server';

-export default class GleanSearchProvider implements SearchProvider {
+export default class GleanSearchProvider implements SearchRanker {
readonly name = 'glean';

async initialize(context: ProviderContext, initData?: SearchProviderInitData) {
if (!process.env.GLEAN_API_TOKEN) throw new Error('GLEAN_API_TOKEN required');
}

- isReady() {
- return Boolean(process.env.GLEAN_API_TOKEN);
- }
-
- async healthCheck() {
- return { healthy: this.isReady() };
- }
-
async search(query: string, options?: SearchOptions): Promise<SearchResult[]> {
return [];
}
}

A provider with no setup can be a plain object:

createWebRequestHandler({
artifacts: bundle,
search: { name: 'glean', search: async (query, options) => [] },
});

Code that drives a provider itself should call the optional members optionally:

-await provider.initialize(context, { bundle });
-if (!provider.isReady()) throw new Error('not ready');
+await provider.initialize?.(context, { bundle });

In docusaurus.config.js, drop the plugin's search option and pass the provider where the server runs:

// docusaurus.config.js
plugins: [
['docusaurus-plugin-mcp-server', {
server: { name: 'my-docs' },
- search: '@myorg/glean-search',
}],
],
// worker.js
+import GleanSearchProvider from '@myorg/glean-search';
+
-createWebRequestHandler({ artifacts: bundle });
+createWebRequestHandler({ artifacts: bundle, search: new GleanSearchProvider() });

This works however the server config gives the provider: as an instance, or as a module path (search: './my-search.js') whose default export is a SearchRanker class or object. (loadSearchProvider(), called directly with a module path, still requires a full SearchProvider through 2.x, since it promises one.)

For agents​

To prepare a project for 3.0.0 (on 2.2 or later), run this checklist in order.

  1. In package.json, make sure docusaurus-plugin-mcp-server is ^2.2.0 or later.
  2. Search the repo for searchIndexData, docsPath, indexPath, and skillsPath:
    • In createWebRequestHandler(...) or new McpDocsServer(...) calls that pass docs/searchIndexData/skills: replace the three imports of build/mcp/*.json with import bundle from '<same dir>/bundle.json', and pass artifacts: bundle instead of those three options.
    • In createNodeServer(...)/createNodeHandler(...) calls that pass docsPath/indexPath/skillsPath: pass artifactsDir: '<the directory those paths were in>' instead.
    • In new McpDocsServer(...) calls that pass docsPath/indexPath: pass artifacts: await readArtifactBundle('<that directory>'), importing readArtifactBundle from docusaurus-plugin-mcp-server/adapters/node.
    • Remove name, version, and baseUrl from those calls only if they match the plugin's server.name, server.version, and the site URL in docusaurus.config.*. Otherwise keep them; they override the build's values (baseUrl only changes what the status reports and providers receive; page URLs come from the build).
  3. In classes that implement SearchProvider, replace reads of initData.docs, initData.indexData, initData.docsPath, and initData.indexPath with initData.bundle.docs and initData.bundle.searchIndex. Files the matching indexer wrote are in initData.bundle.extras, keyed by filename. bundle is optional in the type; guard for it. McpDocsServer sets it once step 2 has moved the server config to artifacts/artifactsDir, so do step 2 first. Where code calls provider.initialize(context, { docsPath, indexPath }) or { docs, indexData } itself, pass { bundle: await readArtifactBundle(dir) } instead.
  4. Replace the deprecated types: McpServerConfig → McpDocsServerConfig (or McpServerBundleConfig), McpServerFileConfig/McpServerDataConfig → McpServerBundleConfig, WebRequestAdapterConfig → WebRequestHandlerConfig, NodeServerOptions → NodeAdapterOptions. Where code reads .name from the new types, handle undefined.
  5. Search deploy scripts and CI config for docs.json, search-index.json, skills.json, and manifest.json under build/mcp. Switch them to bundle.json.
  6. Search the repo for handleHttpRequest(. Replace each server.handleHttpRequest(req, res, body?) route with createNodeHandler(<the config that server was constructed with>) from docusaurus-plugin-mcp-server/adapters/node, mounted on the same path and methods. It sends Access-Control-Allow-Origin: * by default: if that code sets CORS headers itself, or the endpoint must not be callable from other origins' browser pages, pass corsOrigin: false (or a specific origin).
  7. Search the repo for isReady(. For each class or object that implements SearchProvider:
    • First, search for loadSearchProvider( calls that load it by module path (loadSearchProvider('./my-search.js')). Through 2.x those still require a full SearchProvider: change each one to import the module and pass an instance (loadSearchProvider(new MySearch())), or leave this provider's isReady() in place until 3.0 and skip the rest of this step for it. (Server configs that pass it as search: './my-search.js' are fine.)
    • Delete the isReady() method; if it could return false, make initialize() reject in that case instead (or search() throw). If the class's healthCheck() calls this.isReady(), do step 8 for this class now.
    • Change implements SearchProvider to implements SearchRanker (and the import). For an object declared const mySearch: SearchProvider = {...}, replace the annotation with satisfies SearchRanker (if the object defines healthCheck(), do step 8 for it first: satisfies rejects members SearchRanker doesn't have).
    • For other variables, parameters, and fields annotated : SearchProvider that hold it: annotate them with its own type (: MySearch for a class, : typeof mySearch for an object), which keeps every method it defines callable. Or annotate them : SearchRanker, and then call initialize, getDocument, and getDocCount through them optionally (provider.initialize?.(...)): they are optional on SearchRanker.
    • Where code calls provider.isReady() on a provider it drives itself, delete the call.
  8. Search the repo for healthCheck(. Delete healthCheck() methods on search providers, and replace calls to them with the GET status endpoint or McpDocsServer.getStatus().
  9. In docusaurus.config.*, search the plugin options for search:. Delete the option. If its value was not 'local', the plugin option never chose it: check which provider the server config (createWebRequestHandler(...), createNodeServer(...), createNodeHandler(...), or new McpDocsServer(...)) passes as search, and tell the user if it is not the one the plugin option named.
  10. Run docusaurus build, then npx docusaurus-mcp-verify (add --output-dir <dir> if the plugin's outputDir is set). It must pass with no warnings.