mcp-native 0.9.2 → 1.0.0-beta.1

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.
Files changed (3) hide show
  1. package/README.md +32 -78
  2. package/bin/mcp-native.mjs +260 -0
  3. package/package.json +10 -12
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # mcp-native
4
4
 
5
- ### One entry point for the MCP Native React Native host runtime
5
+ ### One entry point for the low-level MCP Native runtime and UI layers
6
6
 
7
7
  [![npm](https://img.shields.io/npm/v/mcp-native)](https://www.npmjs.com/package/mcp-native)
8
8
  [![downloads](https://img.shields.io/npm/dm/mcp-native)](https://www.npmjs.com/package/mcp-native)
@@ -13,23 +13,41 @@
13
13
 
14
14
  </div>
15
15
 
16
- `mcp-native` is the convenient way to use the runtime, A2UI, React Native, mixed-surface, and WebView
17
- APIs from one package. It re-exports the focused `@mcp-native/*` layers while leaving transport
18
- adapters as a separate installation choice.
16
+ `mcp-native` is the convenience entry point for the core, A2UI, React Native, mixed-surface, and
17
+ WebView APIs. It does not include the official MCP SDK adapter or the high-level host. Install
18
+ [`@mcp-native/mcp`](https://www.npmjs.com/package/@mcp-native/mcp) for the adapter or
19
+ [`@mcp-native/host`](https://www.npmjs.com/package/@mcp-native/host) for the connect-call-render
20
+ workflow. Use this package when the application wants to compose the low-level layers itself.
19
21
 
20
- The current 0.9 line contains the React Native feature set planned for 1.0 and is ready to try in an
21
- integration. New work should use A2UI v1 Candidate or the stable MCP Apps `2026-01-26` host flow;
22
- the custom A2UI 0.1 APIs live under `/legacy` for migration.
22
+ This package contains the validated low-level React Native feature set: A2UI v1 Candidate and the
23
+ stable MCP Apps `2026-01-26` host flow. Public standard-contract registration and
24
+ application-defined custom input adapters remain post-1.0 work. Negotiated, locally compiled
25
+ semantic host extensions are already supported.
23
26
 
24
27
  For the big picture, start with the [product guide](https://github.com/pablospaniard/mcp-native/blob/main/docs/product-guide.md).
25
28
 
26
29
  ## Install
27
30
 
31
+ Until the stable `1.0.0` release, select the beta package explicitly:
32
+
28
33
  ```bash
29
- npm install mcp-native react
34
+ npm install mcp-native@beta react
30
35
  ```
31
36
 
32
- Add `react-native` when mounting native surfaces. The package is ESM-only and includes TypeScript declarations.
37
+ React `>=18.1.0` is the only peer dependency. Native components and platform integrations are
38
+ supplied by the host application. The package is ESM-only and includes TypeScript declarations.
39
+
40
+ Run the bundled local diagnostics or generate safe starting points without network access:
41
+
42
+ ```bash
43
+ npx mcp-native doctor
44
+ npx mcp-native scaffold-catalog src/mcp
45
+ npx mcp-native scaffold-extension com.example/data-grid DataGrid src/mcp
46
+ ```
47
+
48
+ Scaffolds refuse to overwrite existing files. The extension command emits a closed, bounded
49
+ manifest and a local React Native registration skeleton; the application must still negotiate it
50
+ and supply explicit policy.
33
51
 
34
52
  ## A2UI v1 Candidate path
35
53
 
@@ -38,78 +56,15 @@ v1 JSONL lifecycle envelopes, maintain bounded ordered surface state, apply expl
38
56
  component/event/function policies, and mount the supported native subset through
39
57
  `A2uiV1NativeSurface`. The mounted surface keeps typed input edits renderer-local and returns validated
40
58
  official action envelopes to a host callback; it never selects a return transport. See the
41
- [complete v1 host-flow example](https://github.com/pablospaniard/mcp-native#a2ui-v1-candidate-host-flow)
59
+ [A2UI package guide](https://github.com/pablospaniard/mcp-native/tree/main/packages/a2ui)
42
60
  and the [`@mcp-native/react-native` adapter documentation](https://github.com/pablospaniard/mcp-native/tree/main/packages/react-native#a2ui-v1-render-plan-adapter).
43
61
 
44
- ## Legacy custom `0.1` migration
45
-
46
- For `0.9.x`, migrate these imports to `mcp-native/legacy`. Root aliases are removed at `1.0.0`;
47
- the explicit legacy subpath stays frozen for migration and security fixes.
48
-
49
- ```tsx
50
- import { McpNativeRuntime, createAllowlistActionPolicy, type McpClient } from "mcp-native";
51
- import {
52
- McpNativeSurface,
53
- parseA2uiSurface,
54
- useMcpNativeActionDispatcher,
55
- } from "mcp-native/legacy";
56
- import { Button, Text, TextInput, View } from "react-native";
57
-
58
- const components = { Button, Text, TextInput, View };
59
-
60
- const client: McpClient = {
61
- async listTools() {
62
- return { tools: [] };
63
- },
64
- async callTool(name, arguments_) {
65
- return {
66
- content: [{ type: "text", text: `Called ${name}` }],
67
- structuredContent: { name, arguments: arguments_ },
68
- };
69
- },
70
- async readResource(uri) {
71
- return { contents: [{ uri, text: "" }] };
72
- },
73
- };
74
-
75
- const runtime = new McpNativeRuntime(client, {
76
- actionPolicy: createAllowlistActionPolicy([{ name: "continue_flow" }]),
77
- });
78
-
79
- const surface = parseA2uiSurface({
80
- version: "0.1",
81
- root: {
82
- id: "welcome",
83
- type: "container",
84
- children: [
85
- { id: "title", type: "text", text: "Hello from MCP" },
86
- {
87
- id: "continue",
88
- type: "button",
89
- label: "Continue",
90
- action: { type: "tool", name: "continue_flow" },
91
- },
92
- ],
93
- },
94
- });
95
-
96
- function NativeScreen() {
97
- const onAction = useMcpNativeActionDispatcher(runtime, {
98
- onError: (error) => console.error("MCP action failed", error),
99
- });
100
-
101
- return <McpNativeSurface surface={surface} components={components} onAction={onAction} />;
102
- }
103
- ```
104
-
105
- The host supplies the locally bundled native components and explicitly allows the tools a surface may dispatch, including their arguments. Without an `actionPolicy`, surface action dispatch is denied; trusted host code can still call `callTool()` directly after JSON validation. MCP Native never downloads and executes server-provided React Native JavaScript.
106
-
107
62
  ## Included packages
108
63
 
109
64
  | Package | What it provides |
110
65
  | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- |
111
66
  | [`@mcp-native/core`](https://www.npmjs.com/package/@mcp-native/core) | MCP client contracts, runtime delegation, JSON types, and declared tool actions. |
112
- | [`@mcp-native/a2ui`](https://www.npmjs.com/package/@mcp-native/a2ui) | Feature-scoped v1 Candidate adapter plus deprecated `0.1` migration APIs. |
67
+ | [`@mcp-native/a2ui`](https://www.npmjs.com/package/@mcp-native/a2ui) | Feature-scoped v1 Candidate negotiation, parsing, and surface state. |
113
68
  | [`@mcp-native/react-native`](https://www.npmjs.com/package/@mcp-native/react-native) | Trusted plans, local v1 state/actions, hooks, and a host-owned component catalog. |
114
69
  | [`@mcp-native/webview`](https://www.npmjs.com/package/@mcp-native/webview) | Stable Apps discovery, sandbox, native adapter, and JSON-RPC bridge. |
115
70
 
@@ -117,10 +72,9 @@ Install an individual package instead when you only need one layer.
117
72
 
118
73
  Use the separately installable [`@mcp-native/mcp`](https://github.com/pablospaniard/mcp-native/tree/main/packages/mcp) package to connect these APIs to the official MCP TypeScript SDK without forcing that SDK dependency on every `mcp-native` consumer.
119
74
 
120
- ## Included in the current 0.9 line
75
+ ## Available across the package line
121
76
 
122
77
  - transport-independent MCP runtime contracts;
123
- - isolated migration support for the deprecated custom `0.1` surface;
124
78
  - declared tool-action dispatch;
125
79
  - strict A2UI resource-link resolution from tool results;
126
80
  - schema-validated A2UI v1 JSONL lifecycle state and explicit host policies;
@@ -147,8 +101,8 @@ Use the separately installable [`@mcp-native/mcp`](https://github.com/pablospani
147
101
 
148
102
  For the release-by-release history, see the
149
103
  [changelog](https://github.com/pablospaniard/mcp-native/blob/main/CHANGELOG.md). The runnable
150
- [Expo Go todo app](../../examples/expo-go-todolist/README.md) shows the main A2UI and React Native
151
- pieces working together.
104
+ [Expo Go todo app](https://github.com/pablospaniard/mcp-native/tree/main/examples/expo-go-todolist)
105
+ shows the main A2UI and React Native pieces working together.
152
106
 
153
107
  ## Security model
154
108
 
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { basename, join, resolve } from "node:path";
5
+
6
+ const [, , command = "doctor", ...arguments_] = process.argv;
7
+
8
+ try {
9
+ if (command === "doctor") {
10
+ runDoctor(arguments_);
11
+ } else if (command === "scaffold-catalog") {
12
+ scaffoldCatalog(arguments_);
13
+ } else if (command === "scaffold-extension") {
14
+ scaffoldExtension(arguments_);
15
+ } else if (command === "help" || command === "--help" || command === "-h") {
16
+ printHelp();
17
+ } else {
18
+ fail(`Unknown command ${JSON.stringify(command)}. Run mcp-native help.`);
19
+ }
20
+ } catch (error) {
21
+ fail(error instanceof Error ? error.message : "MCP Native CLI failed.");
22
+ }
23
+
24
+ function runDoctor(commandArguments) {
25
+ const json = commandArguments.includes("--json");
26
+ const directoryArgument = commandArguments.find((value) => value !== "--json") ?? ".";
27
+ const directory = resolve(directoryArgument);
28
+ const manifestPath = join(directory, "package.json");
29
+ if (!existsSync(manifestPath)) {
30
+ throw new Error(`No package.json found in ${directory}`);
31
+ }
32
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
33
+ const dependencyMaps = [
34
+ manifest.dependencies ?? {},
35
+ manifest.devDependencies ?? {},
36
+ manifest.peerDependencies ?? {},
37
+ ];
38
+ const dependencies = Object.assign({}, ...dependencyMaps);
39
+ const hasWorkspaces = manifest.workspaces !== undefined;
40
+ const mcpNativeEntries = Object.entries(dependencies).filter(
41
+ ([name]) => name === "mcp-native" || name.startsWith("@mcp-native/"),
42
+ );
43
+ const findings = [];
44
+ if (mcpNativeEntries.length === 0) {
45
+ findings.push(
46
+ finding(
47
+ hasWorkspaces ? "warning" : "error",
48
+ hasWorkspaces ? "packages-not-at-workspace-root" : "packages-missing",
49
+ hasWorkspaces
50
+ ? "No MCP Native package is declared at the workspace root; run doctor in the consuming workspace too."
51
+ : "No MCP Native package is declared.",
52
+ ),
53
+ );
54
+ }
55
+ const ranges = new Set(mcpNativeEntries.map(([, range]) => String(range)));
56
+ if (ranges.size > 1) {
57
+ findings.push(
58
+ finding(
59
+ "error",
60
+ "version-ranges-mixed",
61
+ "MCP Native packages use different version ranges; align them before installation.",
62
+ ),
63
+ );
64
+ }
65
+ const usesNative = mcpNativeEntries.some(
66
+ ([name]) =>
67
+ name === "mcp-native" || name === "@mcp-native/react-native" || name === "@mcp-native/host",
68
+ );
69
+ if (usesNative && dependencies.react === undefined) {
70
+ findings.push(
71
+ finding("error", "react-missing", "A React dependency is required by the native renderer."),
72
+ );
73
+ }
74
+ if (usesNative && dependencies["react-native"] === undefined) {
75
+ findings.push(
76
+ finding(
77
+ "warning",
78
+ "react-native-not-declared",
79
+ "No React Native dependency is declared; ignore this only when the package is supplied by the application platform.",
80
+ ),
81
+ );
82
+ }
83
+ const metroPaths = ["metro.config.js", "metro.config.cjs", "metro.config.mjs"].map((name) =>
84
+ join(directory, name),
85
+ );
86
+ if (usesNative && hasWorkspaces && !metroPaths.some(existsSync)) {
87
+ findings.push(
88
+ finding(
89
+ "warning",
90
+ "metro-workspace-config-missing",
91
+ "This workspace has no Metro configuration; verify watchFolders, resolver paths, and duplicate React exclusion.",
92
+ ),
93
+ );
94
+ }
95
+ if (usesNative && !existsSync(join(directory, "tsconfig.json"))) {
96
+ findings.push(
97
+ finding(
98
+ "warning",
99
+ "typescript-config-missing",
100
+ "No tsconfig.json was found, so typed catalog adapters cannot be checked here.",
101
+ ),
102
+ );
103
+ }
104
+ if (findings.length === 0) {
105
+ findings.push(
106
+ finding("ok", "configuration-ready", "No common package or workspace issue found."),
107
+ );
108
+ }
109
+ const report = { directory, packageName: manifest.name ?? basename(directory), findings };
110
+ if (json) {
111
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
112
+ } else {
113
+ process.stdout.write(`MCP Native doctor: ${report.packageName}\n`);
114
+ for (const item of findings) {
115
+ process.stdout.write(`${item.level.toUpperCase()} ${item.code}: ${item.message}\n`);
116
+ }
117
+ }
118
+ if (findings.some((item) => item.level === "error")) process.exitCode = 1;
119
+ }
120
+
121
+ function scaffoldCatalog(commandArguments) {
122
+ const outputDirectory = resolve(commandArguments[0] ?? ".");
123
+ const outputPath = join(outputDirectory, "mcpNativeCatalog.tsx");
124
+ writeNewFile(outputPath, catalogTemplate());
125
+ process.stdout.write(`Created ${outputPath}\n`);
126
+ }
127
+
128
+ function scaffoldExtension(commandArguments) {
129
+ const [extensionId, componentName, directoryArgument = "."] = commandArguments;
130
+ if (
131
+ extensionId === undefined ||
132
+ !/^[a-z0-9]+(?:[._-][a-z0-9]+)+(?:\/[a-z0-9]+(?:[._-][a-z0-9]+)*)?$/u.test(extensionId)
133
+ ) {
134
+ throw new Error("scaffold-extension requires a namespaced extension ID");
135
+ }
136
+ if (componentName === undefined || !/^[A-Z][A-Za-z0-9]*$/u.test(componentName)) {
137
+ throw new Error("scaffold-extension requires a PascalCase component name");
138
+ }
139
+ const outputDirectory = resolve(directoryArgument);
140
+ const manifestPath = join(outputDirectory, `${componentName}.manifest.json`);
141
+ const componentPath = join(outputDirectory, `${componentName}.tsx`);
142
+ assertNewFiles([manifestPath, componentPath]);
143
+ writeNewFile(
144
+ manifestPath,
145
+ `${JSON.stringify(extensionManifest(extensionId, componentName), null, 2)}\n`,
146
+ );
147
+ writeNewFile(componentPath, extensionComponentTemplate(componentName));
148
+ process.stdout.write(`Created ${manifestPath}\nCreated ${componentPath}\n`);
149
+ }
150
+
151
+ function assertNewFiles(paths) {
152
+ const existingPath = paths.find(existsSync);
153
+ if (existingPath !== undefined) {
154
+ throw new Error(`Refusing to overwrite existing file ${existingPath}`);
155
+ }
156
+ }
157
+
158
+ function writeNewFile(path, content) {
159
+ if (existsSync(path)) throw new Error(`Refusing to overwrite existing file ${path}`);
160
+ mkdirSync(resolve(path, ".."), { recursive: true });
161
+ writeFileSync(path, content, { encoding: "utf8", flag: "wx" });
162
+ }
163
+
164
+ function extensionManifest(extensionId, componentName) {
165
+ return {
166
+ profileVersion: "1",
167
+ extensionId,
168
+ catalogId: `${extensionId}@1`,
169
+ catalogVersion: "1",
170
+ schemaVersion: "1.0.0",
171
+ componentName: `${extensionId}:${componentName}`,
172
+ propsSchema: {
173
+ type: "object",
174
+ properties: { label: { type: "string", minLength: 1, maxLength: 128 } },
175
+ required: ["label"],
176
+ additionalProperties: false,
177
+ },
178
+ events: [],
179
+ platforms: ["android", "ios"],
180
+ accessibility: {
181
+ ownership: "host",
182
+ requiresLabel: true,
183
+ behavior: "Expose one host-rendered labeled value.",
184
+ },
185
+ resourceNeeds: [],
186
+ permissionNeeds: [],
187
+ limits: {
188
+ maximumInstances: 16,
189
+ maximumEventPayloadValues: 8,
190
+ maximumEventPayloadStringCodeUnits: 512,
191
+ maximumPropsValues: 16,
192
+ maximumPropsStringCodeUnits: 2048,
193
+ maximumUpdatesPerSurface: 32,
194
+ },
195
+ fallback: { kind: "reject" },
196
+ compatibility: { owner: "Replace with the responsible application team" },
197
+ };
198
+ }
199
+
200
+ function catalogTemplate() {
201
+ return `import { createA2uiV1NativeHost } from "@mcp-native/react-native";
202
+ import { Button, Text, TextInput, View } from "react-native";
203
+
204
+ // Keep this registration at module scope so component identity and local state remain stable.
205
+ export const mcpNativeHost = createA2uiV1NativeHost({
206
+ components: { Button, Text, TextInput, View },
207
+ allowedEventNames: [],
208
+ allowedFunctionNames: [],
209
+ layoutContracts: {
210
+ View: {
211
+ allowedParents: ["bounded", "scroll", "unbounded"],
212
+ sizing: "intrinsic",
213
+ },
214
+ },
215
+ });
216
+ `;
217
+ }
218
+
219
+ function extensionComponentTemplate(componentName) {
220
+ return `import manifestJson from "./${componentName}.manifest.json";
221
+ import { createNativeHostExtensionRegistration } from "@mcp-native/react-native";
222
+ import { Text } from "react-native";
223
+
224
+ function ${componentName}({ label, accessibilityLabel }: { label: string; accessibilityLabel?: string }) {
225
+ return <Text accessibilityLabel={accessibilityLabel}>{label}</Text>;
226
+ }
227
+
228
+ export const ${componentName[0].toLowerCase()}${componentName.slice(1)}Registration =
229
+ createNativeHostExtensionRegistration(
230
+ manifestJson,
231
+ ${componentName},
232
+ ({ accessibilityLabel, semanticProps }) => {
233
+ if (typeof semanticProps.label !== "string") {
234
+ throw new Error("Validated extension props do not match the local component");
235
+ }
236
+ return {
237
+ label: semanticProps.label,
238
+ ...(accessibilityLabel === undefined ? {} : { accessibilityLabel }),
239
+ };
240
+ },
241
+ );
242
+ `;
243
+ }
244
+
245
+ function finding(level, code, message) {
246
+ return { level, code, message };
247
+ }
248
+
249
+ function printHelp() {
250
+ process.stdout.write(`Usage:
251
+ mcp-native doctor [directory] [--json]
252
+ mcp-native scaffold-catalog [output-directory]
253
+ mcp-native scaffold-extension <extension-id> <PascalCaseName> [output-directory]
254
+ `);
255
+ }
256
+
257
+ function fail(message) {
258
+ process.stderr.write(`mcp-native: ${message}\n`);
259
+ process.exitCode = 1;
260
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-native",
3
- "version": "0.9.2",
3
+ "version": "1.0.0-beta.1",
4
4
  "description": "Convenience package for the MCP Native runtime.",
5
5
  "keywords": [
6
6
  "a2ui",
@@ -19,7 +19,11 @@
19
19
  "url": "git+https://github.com/pablospaniard/mcp-native.git",
20
20
  "directory": "packages/mcp-native"
21
21
  },
22
+ "bin": {
23
+ "mcp-native": "./bin/mcp-native.mjs"
24
+ },
22
25
  "files": [
26
+ "bin",
23
27
  "dist",
24
28
  "LICENSE",
25
29
  "README.md"
@@ -44,18 +48,12 @@
44
48
  "access": "public"
45
49
  },
46
50
  "dependencies": {
47
- "@mcp-native/a2ui": "^0.9.2",
48
- "@mcp-native/core": "^0.9.2",
49
- "@mcp-native/react-native": "^0.9.2",
50
- "@mcp-native/webview": "^0.9.2"
51
+ "@mcp-native/a2ui": "^1.0.0-beta.1",
52
+ "@mcp-native/core": "^1.0.0-beta.1",
53
+ "@mcp-native/react-native": "^1.0.0-beta.1",
54
+ "@mcp-native/webview": "^1.0.0-beta.1"
51
55
  },
52
56
  "peerDependencies": {
53
- "react": ">=18.3.1 <20",
54
- "react-native": ">=0.86.0 <1"
55
- },
56
- "peerDependenciesMeta": {
57
- "react-native": {
58
- "optional": true
59
- }
57
+ "react": ">=18.1.0"
60
58
  }
61
59
  }