mcp-layer 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,123 +1,85 @@
1
- # MCP Layer
1
+ # mcp-layer
2
2
 
3
- `mcp-layer` helps engineers bolt extra behaviors on top of existing MCP servers without reimplementing transport or schema parsing. It is a small, focused toolbox for discovering MCP configs, connecting to servers, and extracting a unified schema that can drive CLIs, REST APIs, UI renderers, or additional MCP layers.
3
+ `mcp-layer` is the npm entry point for MCP Layer, a toolkit for building production-ready interfaces on top of any MCP server.
4
4
 
5
- ## Table of Contents
5
+ Use it to discover MCP configs, connect sessions, normalize catalogs, and expose those capabilities through CLI, REST, GraphQL, and OpenAPI layers in one cohesive stack.
6
6
 
7
- - [What you can build with this repo](#what-you-can-build-with-this-repo)
8
- - [Packages](#packages)
9
- - [CLI quick try](#cli-quick-try)
10
- - [Quick start (end-to-end)](#quick-start-end-to-end)
11
- - [Design principles](#design-principles)
12
- - [Development](#development)
13
- - [Releasing](#releasing)
14
- - [Repository guidelines](#repository-guidelines)
7
+ ## Install
15
8
 
16
- ## What you can build with this repo
17
-
18
- - **CLI layers** that expose MCP tools as commands with validated input.
19
- - **REST gateways** that generate endpoints and schemas from MCP tool definitions.
20
- - **GraphQL gateways** that expose generated and generic operations on top of MCP catalogs.
21
- - **UI renderers** that surface MCP Apps resources using `_meta.ui` metadata.
22
- - **New MCP layers** that programmatically reason about tool/resource catalogs.
23
-
24
- ## Packages
25
-
26
- ### mcp-layer
27
-
28
- `mcp-layer` aggregates every workspace package under a single namespace export so you can import from one place when prototyping or composing multiple layers. We still recommend consuming the individual packages by default; the root package exists for teams that want a single install with always-matched versions/majors.
29
-
30
- ```js
31
- import { attach, cli, config, connect, schema, session, testServer } from 'mcp-layer';
32
-
33
- const loaded = await config.load(undefined, process.cwd());
34
- const link = await connect.connect(loaded, 'demo');
35
- const catalog = await schema.extract(link);
36
- await link.close();
37
- ```
38
-
39
- ### Infrastructure and utilities
40
-
41
- | Package | Purpose |
42
- | --- | --- |
43
- | [`@mcp-layer/config`](packages/config/README.md) | Discover and normalize MCP server configs across editors/clients. |
44
- | [`@mcp-layer/session`](packages/session/README.md) | Shared Session handle used by connect and attach. |
45
- | [`@mcp-layer/connect`](packages/connect/README.md) | Connect to MCP servers over stdio and return a closeable Session. |
46
- | [`@mcp-layer/attach`](packages/attach/README.md) | Attach to in-process MCP SDK servers or Platformatic Fastify MCP plugins and return a Session. |
47
- | [`@mcp-layer/schema`](packages/schema/README.md) | Extract tools/resources/prompts/templates into a unified Zod-backed schema (including MCP Apps metadata). |
48
- | [`@mcp-layer/gateway`](packages/gateway/README.md) | Shared adapter runtime primitives for catalog bootstrap, validation, resilience, telemetry, and deterministic mapping. |
49
- | [`@mcp-layer/test-server`](packages/test-server/README.md) | Feature-complete MCP server for integration tests and local exploration. |
50
- | [`@mcp-layer/openapi`](packages/openapi/README.md) | Generate OpenAPI 3.1 specs from MCP catalogs. |
51
- | [`@mcp-layer/rest`](packages/rest/README.md) | Fastify plugin that exposes MCP servers over REST. |
52
-
53
- ### Layers and interfaces
54
-
55
- | Package | Purpose |
56
- | --- | --- |
57
- | [`@mcp-layer/cli`](packages/cli/README.md) | CLI framework for turning MCP schemas into commands. |
58
- | [`@mcp-layer/openapi`](packages/openapi/README.md) | OpenAPI 3.1 generation for REST surfaces. |
59
- | [`@mcp-layer/rest`](packages/rest/README.md) | Fastify plugin for HTTP REST exposure. |
60
- | [`@mcp-layer/graphql`](packages/graphql/README.md) | Fastify plugin and schema builder for GraphQL exposure on MCP catalogs. |
61
-
62
- ## CLI quick try
63
-
64
- If you already have an MCP config on disk, you can try the CLI immediately:
9
+ Use your package manager of choice to install `mcp-layer` in any Node.js project.
65
10
 
66
11
  ```sh
67
- mcp-layer servers list
68
- mcp-layer tools list --server <name>
69
- mcp-layer tools <tool> --help
12
+ npm install mcp-layer
13
+ pnpm add mcp-layer
14
+ yarn add mcp-layer
70
15
  ```
71
16
 
72
- ## Quick start (end-to-end)
17
+ ## Quick Start
73
18
 
74
- ```js
75
- import { load } from '@mcp-layer/config';
76
- import { connect } from '@mcp-layer/connect';
77
- import { extract } from '@mcp-layer/schema';
19
+ This example demonstrates the primary workflow: load MCP configuration, open a session, and extract a normalized catalog from one package import. This matters when you want to wire a prototype quickly without managing multiple top-level dependencies.
78
20
 
79
- const config = await load(undefined, process.cwd());
80
- const session = await connect(config, 'demo');
21
+ ```js
22
+ import { config, connect, schema } from 'mcp-layer';
81
23
 
82
- const schema = await extract(session);
83
- console.log(schema.items.map((item) => item.name));
24
+ const cfg = await config.load(undefined, process.cwd());
25
+ const session = await connect.connect(cfg, 'demo');
26
+ const catalog = await schema.extract(session);
84
27
 
28
+ console.log(catalog.items?.length ?? 0);
85
29
  await session.close();
86
30
  ```
87
31
 
88
- ## Design principles
32
+ Running this script prints the number of discovered catalog items for the selected server.
89
33
 
90
- - **Single source of truth**: normalized schemas flow from the MCP server, not from tool-specific parsing.
91
- - **Layered adapters**: `schema -> gateway -> rest/graphql` keeps protocol logic centralized while adapters stay thin.
92
- - **Minimal surface area**: each package does one job and composes cleanly with the others.
93
- - **Real integration tests**: no mocks; the test server is used as a real MCP target.
34
+ ## API Reference
94
35
 
95
- ## Development
36
+ `mcp-layer` exports package namespaces. If you are new to MCP Layer, this table explains what each namespace is for and links to full package documentation.
96
37
 
97
- ```sh
98
- pnpm install
99
- pnpm test
100
- ```
38
+ | Namespace export | Purpose | Documentation |
39
+ | --- | --- | --- |
40
+ | `attach` | Attach to in-process MCP SDK servers without launching external transports. | [`@mcp-layer/attach` README](https://github.com/3rd-Eden/mcp-layer/blob/main/packages/attach/README.md) |
41
+ | `cli` | Build command-line interfaces for MCP tools, prompts, resources, and templates. | [`@mcp-layer/cli` README](https://github.com/3rd-Eden/mcp-layer/blob/main/packages/cli/README.md) |
42
+ | `config` | Discover and normalize MCP server configuration across host tools. | [`@mcp-layer/config` README](https://github.com/3rd-Eden/mcp-layer/blob/main/packages/config/README.md) |
43
+ | `connect` | Connect to MCP servers over stdio, Streamable HTTP, or SSE. | [`@mcp-layer/connect` README](https://github.com/3rd-Eden/mcp-layer/blob/main/packages/connect/README.md) |
44
+ | `error` | Create structured runtime errors with stable references and docs links. | [`@mcp-layer/error` README](https://github.com/3rd-Eden/mcp-layer/blob/main/packages/error/README.md) |
45
+ | `gateway` | Reuse shared adapter runtime primitives for mapping, validation, resilience, and telemetry. | [`@mcp-layer/gateway` README](https://github.com/3rd-Eden/mcp-layer/blob/main/packages/gateway/README.md) |
46
+ | `graphql` | Expose MCP catalogs and operations through GraphQL. | [`@mcp-layer/graphql` README](https://github.com/3rd-Eden/mcp-layer/blob/main/packages/graphql/README.md) |
47
+ | `manager` | Manage authenticated and shared MCP sessions with reuse and limits. | [`@mcp-layer/manager` README](https://github.com/3rd-Eden/mcp-layer/blob/main/packages/manager/README.md) |
48
+ | `openapi` | Generate OpenAPI 3.1 documents from MCP catalogs. | [`@mcp-layer/openapi` README](https://github.com/3rd-Eden/mcp-layer/blob/main/packages/openapi/README.md) |
49
+ | `rest` | Expose MCP sessions as REST endpoints with Fastify. | [`@mcp-layer/rest` README](https://github.com/3rd-Eden/mcp-layer/blob/main/packages/rest/README.md) |
50
+ | `schema` | Extract and normalize tools, resources, prompts, and templates into a consistent catalog. | [`@mcp-layer/schema` README](https://github.com/3rd-Eden/mcp-layer/blob/main/packages/schema/README.md) |
51
+ | `session` | Use the shared session abstraction across connectors and adapters. | [`@mcp-layer/session` README](https://github.com/3rd-Eden/mcp-layer/blob/main/packages/session/README.md) |
52
+ | `testServer` | Run a feature-complete local MCP server for development and integration tests. | [`@mcp-layer/test-server` README](https://github.com/3rd-Eden/mcp-layer/blob/main/packages/test-server/README.md) |
53
+
54
+ ### Error Behavior
55
+
56
+ This package does not introduce a custom runtime layer. Errors are raised by whichever namespace package you call.
101
57
 
102
- ## Releasing
58
+ For structured runtime errors, refer to the package-level error sections in the linked namespace READMEs.
103
59
 
104
- We use Changesets to manage versions and a GitHub Actions release workflow to publish packages. In short:
60
+ <a id="error-58ed2a"></a>
61
+ #### `findWorkspaceRoot`: Unable to locate workspace root from mcp-layer package.
105
62
 
106
- 1) Add a changeset: `pnpm changeset`
107
- 2) Merge the Changesets PR
108
- 3) GitHub Actions publishes via npm Trusted Publishing (OIDC)
63
+ This error is raised when `mcp-layer` package generation runs in a directory tree that does not contain `pnpm-workspace.yaml`.
109
64
 
110
- Before publishing, configure npm Trusted Publishers for each package in the npm registry and ensure the release workflow has `id-token: write` permissions.
65
+ Step-by-step resolution:
66
+ 1. Run generation from inside a checkout of the MCP Layer repository.
67
+ 2. Confirm `pnpm-workspace.yaml` exists at the repository root.
68
+ 3. Run `pnpm --filter mcp-layer prepack` from the repository root.
111
69
 
112
- The root package runs `prepack` to regenerate `src/index.js` and sync root `package.json` dependencies to the workspace packages before publishing.
70
+ <details>
71
+ <summary>Debug example</summary>
113
72
 
114
- See `.github/workflows/release.yml` for the pipeline details.
73
+ If you copy `packages/mcp-layer` into a standalone folder and run `node src/generate.js`, `findWorkspaceRoot` cannot locate the workspace marker and throws this error.
74
+ </details>
115
75
 
116
- The publish guard at `.github/workflows/publish-guard.yml` runs after the `Release` workflow completes and locates the open `Version Packages` PR (`changeset-release/main`). It checks npm for workspace package existence and upserts a single PR comment when packages still need first-time bootstrap publishing.
76
+ ## Standards and Portability
117
77
 
118
- ## Repository guidelines
78
+ MCP behavior follows the official MCP specification and official TypeScript SDK contracts:
119
79
 
120
- See `AGENTS.md` for coding style, testing requirements, and documentation expectations.
80
+ - [Model Context Protocol specification](https://modelcontextprotocol.io/specification)
81
+ - [MCP transport specification](https://modelcontextprotocol.io/specification/latest/basic/transports)
82
+ - [Model Context Protocol TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
121
83
 
122
84
  ## License
123
85
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mcp-layer",
3
- "version": "0.2.1",
4
- "description": "Layers of functionality that can be add to any MCP server. ",
3
+ "version": "0.2.2",
4
+ "description": "Build production-ready CLI, REST, GraphQL, and OpenAPI interfaces on top of any MCP server.",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/3rd-Eden/mcp-layer"
@@ -23,16 +23,28 @@
23
23
  ],
24
24
  "license": "MIT",
25
25
  "author": "Arnout Kazemier",
26
- "packageManager": "pnpm@8.15.4",
27
26
  "type": "module",
28
- "devDependencies": {
29
- "@changesets/cli": "^2.29.2"
27
+ "main": "./src/index.js",
28
+ "exports": {
29
+ ".": {
30
+ "import": "./src/index.js",
31
+ "default": "./src/index.js"
32
+ },
33
+ "./package.json": "./package.json"
34
+ },
35
+ "files": [
36
+ "src",
37
+ "README.md",
38
+ "LICENSE"
39
+ ],
40
+ "publishConfig": {
41
+ "access": "public"
30
42
  },
31
43
  "dependencies": {
32
44
  "@mcp-layer/attach": "1.1.1",
33
- "@mcp-layer/cli": "1.2.0",
45
+ "@mcp-layer/cli": "1.3.0",
34
46
  "@mcp-layer/config": "1.0.1",
35
- "@mcp-layer/connect": "1.1.0",
47
+ "@mcp-layer/connect": "1.2.0",
36
48
  "@mcp-layer/error": "0.2.0",
37
49
  "@mcp-layer/manager": "0.2.0",
38
50
  "@mcp-layer/graphql": "0.2.0",
@@ -41,21 +53,9 @@
41
53
  "@mcp-layer/rest": "1.2.1",
42
54
  "@mcp-layer/schema": "1.0.2",
43
55
  "@mcp-layer/session": "1.0.0",
44
- "@mcp-layer/test-server": "1.0.2"
45
- },
46
- "main": "./src/index.js",
47
- "files": [
48
- "src",
49
- "README.md",
50
- "LICENSE"
51
- ],
52
- "exports": {
53
- ".": "./src/index.js"
56
+ "@mcp-layer/test-server": "1.0.3"
54
57
  },
55
58
  "scripts": {
56
- "test": "node --test",
57
- "changeset": "changeset",
58
- "changeset:version": "changeset version",
59
- "changeset:publish": "changeset publish"
59
+ "test": "node --test ./test/**/*.js"
60
60
  }
61
61
  }
package/src/generate.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { access, readFile, readdir, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
+ import { LayerError } from '@mcp-layer/error';
4
5
 
5
6
  /**
6
7
  * Convert a package folder name into a namespace export name.
@@ -55,8 +56,8 @@ export async function hasPackageJson(pkgPath) {
55
56
  * @param {string} rootDir - Repository root directory.
56
57
  * @returns {Promise<string>} Generated index.js content.
57
58
  */
58
- export async function buildIndex(rootDir) {
59
- const meta = await collectPackageMeta(rootDir);
59
+ export async function buildIndex(rootDir, selfName = 'mcp-layer') {
60
+ const meta = await collectPackageMeta(rootDir, new Set([selfName]));
60
61
  const lines = [];
61
62
 
62
63
  lines.push('/**');
@@ -84,15 +85,17 @@ export async function buildIndex(rootDir) {
84
85
  /**
85
86
  * Collect package metadata from workspace folders.
86
87
  * @param {string} rootDir - Repository root directory.
88
+ * @param {Set<string>} omit - Package names to skip.
87
89
  * @returns {Promise<Array<{name: string, dir: string}>>} Package metadata.
88
90
  */
89
- export async function collectPackageMeta(rootDir) {
91
+ export async function collectPackageMeta(rootDir, omit = new Set()) {
90
92
  const packages = await collectPackages(rootDir);
91
93
  const meta = [];
92
94
 
93
95
  for (const dir of packages) {
94
96
  const pkgPath = path.join(rootDir, 'packages', dir, 'package.json');
95
97
  const data = await readJson(pkgPath);
98
+ if (omit.has(data.name)) continue;
96
99
  meta.push({ name: data.name, dir });
97
100
  }
98
101
 
@@ -100,18 +103,21 @@ export async function collectPackageMeta(rootDir) {
100
103
  }
101
104
 
102
105
  /**
103
- * Update root package.json dependencies to match workspace packages.
106
+ * Update mcp-layer package dependencies to match workspace packages.
107
+ * @param {string} pkgDir - mcp-layer package directory.
104
108
  * @param {string} rootDir - Repository root directory.
109
+ * @param {string} selfName - mcp-layer package name.
105
110
  * @returns {Promise<void>} Resolves when dependencies are updated.
106
111
  */
107
- export async function syncDependencies(rootDir) {
108
- const pkgPath = path.join(rootDir, 'package.json');
112
+ export async function syncDependencies(pkgDir, rootDir, selfName = 'mcp-layer') {
113
+ const pkgPath = path.join(pkgDir, 'package.json');
109
114
  const data = await readJson(pkgPath);
110
115
  const deps = { ...(data.dependencies ?? {}) };
111
- const meta = await collectPackageMeta(rootDir);
116
+ const meta = await collectPackageMeta(rootDir, new Set([selfName]));
112
117
  const names = new Set();
113
118
 
114
119
  for (const item of meta) {
120
+ if (!item.name.startsWith('@mcp-layer/')) continue;
115
121
  names.add(item.name);
116
122
  deps[item.name] = 'workspace:*';
117
123
  }
@@ -130,8 +136,8 @@ export async function syncDependencies(rootDir) {
130
136
  * @param {string} outFile - Output file path.
131
137
  * @returns {Promise<void>} Resolves when the file is written or unchanged.
132
138
  */
133
- export async function writeIndex(rootDir, outFile) {
134
- const next = await buildIndex(rootDir);
139
+ export async function writeIndex(rootDir, outFile, selfName = 'mcp-layer') {
140
+ const next = await buildIndex(rootDir, selfName);
135
141
  const current = await readMaybe(outFile);
136
142
 
137
143
  if (current === next) return;
@@ -193,16 +199,43 @@ export function sortObject(input) {
193
199
  return sorted;
194
200
  }
195
201
 
202
+ /**
203
+ * Find the workspace root directory by locating pnpm-workspace.yaml.
204
+ * @param {string} start - Directory to start searching from.
205
+ * @returns {Promise<string>} Workspace root directory.
206
+ */
207
+ export async function findWorkspaceRoot(start) {
208
+ let current = path.resolve(start);
209
+
210
+ while (true) {
211
+ const marker = path.join(current, 'pnpm-workspace.yaml');
212
+ if (await hasPackageJson(marker)) return current;
213
+
214
+ const parent = path.dirname(current);
215
+ if (parent === current)
216
+ throw new LayerError({
217
+ name: 'mcp-layer',
218
+ method: 'findWorkspaceRoot',
219
+ message: 'Unable to locate workspace root from mcp-layer package.',
220
+ });
221
+
222
+ current = parent;
223
+ }
224
+ }
225
+
196
226
  /**
197
227
  * Run the generator when executed as a script.
198
228
  * @returns {Promise<void>} Resolves when generation completes.
199
229
  */
200
230
  export async function run() {
201
- const rootDir = process.cwd();
202
- const outFile = path.join(rootDir, 'src', 'index.js');
203
-
204
- await syncDependencies(rootDir);
205
- await writeIndex(rootDir, outFile);
231
+ const fileDir = path.dirname(fileURLToPath(import.meta.url));
232
+ const pkgDir = path.resolve(fileDir, '..');
233
+ const rootDir = await findWorkspaceRoot(pkgDir);
234
+ const outFile = path.join(pkgDir, 'src', 'index.js');
235
+ const data = await readJson(path.join(pkgDir, 'package.json'));
236
+
237
+ await syncDependencies(pkgDir, rootDir, data.name);
238
+ await writeIndex(rootDir, outFile, data.name);
206
239
  }
207
240
 
208
241
  const entry = fileURLToPath(import.meta.url);
@@ -1,53 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { readFile } from 'node:fs/promises';
3
- import path from 'node:path';
4
- import test from 'node:test';
5
- import { buildIndex, collectPackageMeta, readJson } from './generate.js';
6
-
7
- /**
8
- * Ensure the generated index.js matches the committed file.
9
- * @returns {Promise<void>}
10
- */
11
- async function matchesGeneratedOutput() {
12
- const rootDir = process.cwd();
13
- const outFile = path.join(rootDir, 'src', 'index.js');
14
- const expected = await buildIndex(rootDir);
15
- const actual = await readFile(outFile, 'utf8');
16
-
17
- assert.equal(actual, expected);
18
- }
19
-
20
- test('mcp-layer index matches generator output', matchesGeneratedOutput);
21
-
22
- /**
23
- * Ensure root package.json dependencies include all workspace packages.
24
- * @returns {Promise<void>}
25
- */
26
- async function matchesDependencies() {
27
- const rootDir = process.cwd();
28
- const pkgPath = path.join(rootDir, 'package.json');
29
- const rootPkg = await readJson(pkgPath);
30
- const deps = rootPkg.dependencies ?? {};
31
- const meta = await collectPackageMeta(rootDir);
32
- const names = new Set();
33
-
34
- for (const item of meta) {
35
- names.add(item.name);
36
- assert.equal(
37
- deps[item.name],
38
- 'workspace:*',
39
- `Expected dependency ${item.name} to be set to workspace:*`,
40
- );
41
- }
42
-
43
- for (const key of Object.keys(deps)) {
44
- if (key.startsWith('@mcp-layer/')) {
45
- assert.ok(
46
- names.has(key),
47
- `Expected dependency ${key} to map to a workspace package`,
48
- );
49
- }
50
- }
51
- }
52
-
53
- test('mcp-layer dependencies include workspace packages', matchesDependencies);