mcp-native 0.9.3 → 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.
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,28 +13,42 @@
13
13
 
14
14
  </div>
15
15
 
16
- `mcp-native` is the current convenient way to use the runtime, A2UI, React Native, mixed-surface,
17
- and WebView APIs from one package. It re-exports the focused `@mcp-native/*` layers while leaving
18
- transport adapters as a separate installation choice. The `1.0.0` roadmap adds
19
- `@mcp-native/host` as an optional high-level connect-call-render package; these low-level APIs remain
20
- available for applications that need manual composition.
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.
21
21
 
22
- The current 0.9 line contains the validated low-level React Native feature set and is ready to try in
23
- an integration. New work should use A2UI v1 Candidate or the stable MCP Apps `2026-01-26` host flow;
24
- the custom A2UI 0.1 APIs live under `/legacy` for migration. Public standard-contract registration
25
- and application-defined custom input adapters are explicitly deferred until after `1.0.0`.
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.
26
26
 
27
27
  For the big picture, start with the [product guide](https://github.com/pablospaniard/mcp-native/blob/main/docs/product-guide.md).
28
28
 
29
29
  ## Install
30
30
 
31
+ Until the stable `1.0.0` release, select the beta package explicitly:
32
+
31
33
  ```bash
32
- npm install mcp-native react
34
+ npm install mcp-native@beta react
33
35
  ```
34
36
 
35
37
  React `>=18.1.0` is the only peer dependency. Native components and platform integrations are
36
38
  supplied by the host application. The package is ESM-only and includes TypeScript declarations.
37
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.
51
+
38
52
  ## A2UI v1 Candidate path
39
53
 
40
54
  The package re-exports the APIs needed to negotiate the project-owned binding, resolve official
@@ -42,78 +56,15 @@ v1 JSONL lifecycle envelopes, maintain bounded ordered surface state, apply expl
42
56
  component/event/function policies, and mount the supported native subset through
43
57
  `A2uiV1NativeSurface`. The mounted surface keeps typed input edits renderer-local and returns validated
44
58
  official action envelopes to a host callback; it never selects a return transport. See the
45
- [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)
46
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).
47
61
 
48
- ## Legacy custom `0.1` migration
49
-
50
- For `0.9.x`, migrate these imports to `mcp-native/legacy`. Root aliases are removed at `1.0.0`;
51
- the explicit legacy subpath stays frozen for migration and security fixes.
52
-
53
- ```tsx
54
- import { McpNativeRuntime, createAllowlistActionPolicy, type McpClient } from "mcp-native";
55
- import {
56
- McpNativeSurface,
57
- parseA2uiSurface,
58
- useMcpNativeActionDispatcher,
59
- } from "mcp-native/legacy";
60
- import { Button, Text, TextInput, View } from "react-native";
61
-
62
- const components = { Button, Text, TextInput, View };
63
-
64
- const client: McpClient = {
65
- async listTools() {
66
- return { tools: [] };
67
- },
68
- async callTool(name, arguments_) {
69
- return {
70
- content: [{ type: "text", text: `Called ${name}` }],
71
- structuredContent: { name, arguments: arguments_ },
72
- };
73
- },
74
- async readResource(uri) {
75
- return { contents: [{ uri, text: "" }] };
76
- },
77
- };
78
-
79
- const runtime = new McpNativeRuntime(client, {
80
- actionPolicy: createAllowlistActionPolicy([{ name: "continue_flow" }]),
81
- });
82
-
83
- const surface = parseA2uiSurface({
84
- version: "0.1",
85
- root: {
86
- id: "welcome",
87
- type: "container",
88
- children: [
89
- { id: "title", type: "text", text: "Hello from MCP" },
90
- {
91
- id: "continue",
92
- type: "button",
93
- label: "Continue",
94
- action: { type: "tool", name: "continue_flow" },
95
- },
96
- ],
97
- },
98
- });
99
-
100
- function NativeScreen() {
101
- const onAction = useMcpNativeActionDispatcher(runtime, {
102
- onError: (error) => console.error("MCP action failed", error),
103
- });
104
-
105
- return <McpNativeSurface surface={surface} components={components} onAction={onAction} />;
106
- }
107
- ```
108
-
109
- 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.
110
-
111
62
  ## Included packages
112
63
 
113
64
  | Package | What it provides |
114
65
  | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- |
115
66
  | [`@mcp-native/core`](https://www.npmjs.com/package/@mcp-native/core) | MCP client contracts, runtime delegation, JSON types, and declared tool actions. |
116
- | [`@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. |
117
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. |
118
69
  | [`@mcp-native/webview`](https://www.npmjs.com/package/@mcp-native/webview) | Stable Apps discovery, sandbox, native adapter, and JSON-RPC bridge. |
119
70
 
@@ -121,10 +72,9 @@ Install an individual package instead when you only need one layer.
121
72
 
122
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.
123
74
 
124
- ## Included in the current 0.9 line
75
+ ## Available across the package line
125
76
 
126
77
  - transport-independent MCP runtime contracts;
127
- - isolated migration support for the deprecated custom `0.1` surface;
128
78
  - declared tool-action dispatch;
129
79
  - strict A2UI resource-link resolution from tool results;
130
80
  - schema-validated A2UI v1 JSONL lifecycle state and explicit host policies;
@@ -151,8 +101,8 @@ Use the separately installable [`@mcp-native/mcp`](https://github.com/pablospani
151
101
 
152
102
  For the release-by-release history, see the
153
103
  [changelog](https://github.com/pablospaniard/mcp-native/blob/main/CHANGELOG.md). The runnable
154
- [Expo Go todo app](../../examples/expo-go-todolist/README.md) shows the main A2UI and React Native
155
- 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.
156
106
 
157
107
  ## Security model
158
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.3",
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,10 +48,10 @@
44
48
  "access": "public"
45
49
  },
46
50
  "dependencies": {
47
- "@mcp-native/a2ui": "^0.9.3",
48
- "@mcp-native/core": "^0.9.3",
49
- "@mcp-native/react-native": "^0.9.3",
50
- "@mcp-native/webview": "^0.9.3"
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
57
  "react": ">=18.1.0"