@runsnative/mcp-server 0.6.0 → 0.7.0

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.
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The session-ENACT tool set — every MCP tool that submits a command into a
3
+ * linked session (Copresence Protocol §5.1 `target: 'session'` with an
4
+ * `enactment`). Single source of truth (JUNE-739): server.ts registers from
5
+ * this list, copresence-meta.test.ts asserts annotations over it, and the
6
+ * cross-surface reconciliation test (mcp-api-worker
7
+ * test/cross-surface-reconciliation.test.ts) binds it to the worker
8
+ * allow-list, entitlement registry, and org DISPATCH_TABLE.
9
+ *
10
+ * Adding a session verb? Add the tool HERE (never only in server.ts) — the
11
+ * reconciliation test fails until all four surfaces carry it.
12
+ *
13
+ * NOT in this list: read-leg session tools with no enactment
14
+ * (get_marker_capture, fetch_marker_crop, render_marker_capture) and the
15
+ * binding-redemption tool (link_session).
16
+ */
17
+ import { SET_THEME_TOOL } from './set-theme.js';
18
+ import { SET_SKIN_TOOL } from './set-skin.js';
19
+ import { SET_MODE_TOOL } from './set-mode.js';
20
+ import { SET_CONTRAST_TOOL } from './set-contrast.js';
21
+ import { APPLY_INFERRED_THEME_TOOL } from './apply-inferred-theme.js';
22
+ import { NAVIGATE_TOOL } from './navigate.js';
23
+ import { SET_INSTANCE_VARIANT_TOOL } from './set-instance-variant.js';
24
+ import { SPOTLIGHT_TOOL } from './spotlight.js';
25
+ export const SESSION_ENACT_TOOLS = [
26
+ SET_THEME_TOOL,
27
+ SET_SKIN_TOOL,
28
+ SET_MODE_TOOL,
29
+ SET_CONTRAST_TOOL,
30
+ APPLY_INFERRED_THEME_TOOL,
31
+ NAVIGATE_TOOL,
32
+ SET_INSTANCE_VARIANT_TOOL,
33
+ SPOTLIGHT_TOOL,
34
+ ];
35
+ /** The `capability.method` allow-list entries this tool set can enact. */
36
+ export function listSessionEnactments() {
37
+ return SESSION_ENACT_TOOLS.map((t) => t._meta?.copresence?.enactment).filter((e) => typeof e === 'string');
38
+ }
@@ -0,0 +1,59 @@
1
+ import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
2
+ import { postWorkerApi, getLinkedPairId } from '../bridge-client.js';
3
+ export const SET_CONTRAST_TOOL = {
4
+ name: 'set_contrast',
5
+ description: "Switch the user's linked runsnative.org tab between standard and high contrast — the accessibility axis of the " +
6
+ 'theme, orthogonal to light/dark mode. Use when the user asks for higher contrast, better legibility, or to go ' +
7
+ 'back to the standard rendering. The command is enqueued and the tab applies it within one poll interval; ' +
8
+ 'nothing is saved — fully reversible. Requires an active linked session (call link_session first).',
9
+ inputSchema: {
10
+ type: 'object',
11
+ properties: {
12
+ contrast: { type: 'string', enum: ['standard', 'high'], description: "Contrast to apply: 'standard' or 'high'." },
13
+ },
14
+ required: ['contrast'],
15
+ },
16
+ // Copresence Protocol §5.1 — session-targeting capability; `enactment` names
17
+ // the worker allow-list entry this tool submits (JUNE-626 / JUNE-739).
18
+ _meta: {
19
+ copresence: {
20
+ target: 'session',
21
+ tier: 'reversible',
22
+ consent: 'implicit',
23
+ entitlement: 'free',
24
+ visibility: 'advertised',
25
+ enactment: 'theme.setContrast',
26
+ },
27
+ },
28
+ };
29
+ export async function handleSetContrast(args) {
30
+ const contrast = args['contrast'];
31
+ if (contrast !== 'standard' && contrast !== 'high') {
32
+ throw new McpError(ErrorCode.InvalidParams, "contrast must be 'standard' or 'high'");
33
+ }
34
+ const pairId = getLinkedPairId();
35
+ if (!pairId) {
36
+ throw new McpError(ErrorCode.InvalidParams, 'No linked session. Call link_session with the pairing code first, or open runsnative.org and start the pairing flow.');
37
+ }
38
+ const res = await postWorkerApi('/tenant/command', {
39
+ pair_id: pairId,
40
+ capability: 'theme',
41
+ method: 'setContrast',
42
+ args: [contrast],
43
+ });
44
+ if (res.status === 404) {
45
+ throw new McpError(ErrorCode.InvalidParams, 'Linked session not found or expired. Re-link with link_session.');
46
+ }
47
+ if (res.status === 422) {
48
+ const body = await res.json().catch(() => ({}));
49
+ throw new McpError(ErrorCode.InvalidParams, body.error ?? 'Command rejected by allow-list.');
50
+ }
51
+ if (res.status === 409) {
52
+ throw new McpError(ErrorCode.InvalidParams, 'No live tab detected. Open runsnative.org, ensure the tab is active, then retry.');
53
+ }
54
+ if (!res.ok) {
55
+ const body = await res.text().catch(() => '');
56
+ throw new McpError(ErrorCode.InternalError, `Worker error ${res.status}: ${body}`);
57
+ }
58
+ return { content: [{ type: 'text', text: `Contrast command enqueued: ${contrast}. The tab will apply it within one poll interval.` }] };
59
+ }
@@ -0,0 +1,59 @@
1
+ import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
2
+ import { postWorkerApi, getLinkedPairId } from '../bridge-client.js';
3
+ export const SET_MODE_TOOL = {
4
+ name: 'set_mode',
5
+ description: "Switch the user's linked runsnative.org tab between light and dark mode — the luminance axis of the theme, " +
6
+ 'orthogonal to contrast. Use when the user asks for dark mode, light mode, or to compare the two. The command ' +
7
+ 'is enqueued and the tab applies it within one poll interval; nothing is saved — fully reversible. Requires an ' +
8
+ 'active linked session (call link_session first).',
9
+ inputSchema: {
10
+ type: 'object',
11
+ properties: {
12
+ mode: { type: 'string', enum: ['light', 'dark'], description: "Mode to apply: 'light' or 'dark'." },
13
+ },
14
+ required: ['mode'],
15
+ },
16
+ // Copresence Protocol §5.1 — session-targeting capability; `enactment` names
17
+ // the worker allow-list entry this tool submits (JUNE-626 / JUNE-739).
18
+ _meta: {
19
+ copresence: {
20
+ target: 'session',
21
+ tier: 'reversible',
22
+ consent: 'implicit',
23
+ entitlement: 'free',
24
+ visibility: 'advertised',
25
+ enactment: 'theme.setMode',
26
+ },
27
+ },
28
+ };
29
+ export async function handleSetMode(args) {
30
+ const mode = args['mode'];
31
+ if (mode !== 'light' && mode !== 'dark') {
32
+ throw new McpError(ErrorCode.InvalidParams, "mode must be 'light' or 'dark'");
33
+ }
34
+ const pairId = getLinkedPairId();
35
+ if (!pairId) {
36
+ throw new McpError(ErrorCode.InvalidParams, 'No linked session. Call link_session with the pairing code first, or open runsnative.org and start the pairing flow.');
37
+ }
38
+ const res = await postWorkerApi('/tenant/command', {
39
+ pair_id: pairId,
40
+ capability: 'theme',
41
+ method: 'setMode',
42
+ args: [mode],
43
+ });
44
+ if (res.status === 404) {
45
+ throw new McpError(ErrorCode.InvalidParams, 'Linked session not found or expired. Re-link with link_session.');
46
+ }
47
+ if (res.status === 422) {
48
+ const body = await res.json().catch(() => ({}));
49
+ throw new McpError(ErrorCode.InvalidParams, body.error ?? 'Command rejected by allow-list.');
50
+ }
51
+ if (res.status === 409) {
52
+ throw new McpError(ErrorCode.InvalidParams, 'No live tab detected. Open runsnative.org, ensure the tab is active, then retry.');
53
+ }
54
+ if (!res.ok) {
55
+ const body = await res.text().catch(() => '');
56
+ throw new McpError(ErrorCode.InternalError, `Worker error ${res.status}: ${body}`);
57
+ }
58
+ return { content: [{ type: 'text', text: `Mode command enqueued: ${mode}. The tab will apply it within one poll interval.` }] };
59
+ }
@@ -0,0 +1,69 @@
1
+ import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
2
+ import { postWorkerApi } from '../bridge-client.js';
3
+ // An ORDINARY (non-session-targeting) authenticated MCP tool. Unlike the
4
+ // session-ENACT tools (set_theme, set_instance_variant, …), this does NOT drive a
5
+ // live bound tab through a pair — it persists content to the caller's PREVIEW
6
+ // namespace via the tenant token, and the preview site renders it on next load.
7
+ // Per Copresence Protocol §5.1, a tool that does not target a bound live session
8
+ // is an ordinary MCP tool, so it carries NO _meta.copresence and is NOT in
9
+ // SESSION_ENACT_TOOLS (see copresence-meta.test.ts: "Content tools likewise").
10
+ export const SET_SITE_CONTENT_TOOL = {
11
+ name: 'set_site_content',
12
+ description: "Edit a section of the user's own site content in their PREVIEW environment. The change is " +
13
+ 'saved to preview ONLY — the staging and production versions of the site are never touched — and ' +
14
+ 'the preview site shows it on next load. Use this for content edits the user describes in words ' +
15
+ '(e.g. "we\'re past the summer closings, remove them from les heures"). Provide the section key and ' +
16
+ 'the FULL new content object for that section — it replaces the current preview content. The shape ' +
17
+ 'is section-specific and validated server-side; an invalid shape is rejected. Requires an ' +
18
+ 'authenticated RunsNative token whose role may edit this site.',
19
+ inputSchema: {
20
+ type: 'object',
21
+ properties: {
22
+ section_key: {
23
+ type: 'string',
24
+ description: "The section to edit, page-namespaced, e.g. 'index:les-heures'.",
25
+ },
26
+ content: {
27
+ type: 'object',
28
+ description: 'The full replacement content object for the section (section-specific shape, validated ' +
29
+ 'server-side).',
30
+ },
31
+ },
32
+ required: ['section_key', 'content'],
33
+ },
34
+ };
35
+ export async function handleSetSiteContent(args) {
36
+ const sectionKey = args['section_key'];
37
+ if (typeof sectionKey !== 'string' || !sectionKey.trim()) {
38
+ throw new McpError(ErrorCode.InvalidParams, 'section_key must be a non-empty string');
39
+ }
40
+ const content = args['content'];
41
+ if (!content || typeof content !== 'object' || Array.isArray(content)) {
42
+ throw new McpError(ErrorCode.InvalidParams, 'content must be an object');
43
+ }
44
+ const res = await postWorkerApi('/content', { section_key: sectionKey.trim(), content });
45
+ if (res.status === 401) {
46
+ throw new McpError(ErrorCode.InvalidParams, 'Not authenticated. Set RUNSNATIVE_TENANT_TOKEN to a token that may edit this site.');
47
+ }
48
+ if (res.status === 403) {
49
+ throw new McpError(ErrorCode.InvalidParams, 'Your role does not permit editing this site.');
50
+ }
51
+ if (res.status === 400) {
52
+ const body = (await res.json().catch(() => ({})));
53
+ throw new McpError(ErrorCode.InvalidParams, body.error ?? 'Content rejected by the server.');
54
+ }
55
+ if (!res.ok) {
56
+ const body = await res.text().catch(() => '');
57
+ throw new McpError(ErrorCode.InternalError, `Worker error ${res.status}: ${body}`);
58
+ }
59
+ const body = (await res.json());
60
+ return {
61
+ content: [
62
+ {
63
+ type: 'text',
64
+ text: `Saved "${body.section_key}" to your preview site (version ${body.version}). ` +
65
+ 'Reload the preview to see it — staging and production are unchanged.',
66
+ },
67
+ ],
68
+ };
69
+ }
@@ -0,0 +1,60 @@
1
+ import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
2
+ import { postWorkerApi, getLinkedPairId } from '../bridge-client.js';
3
+ export const SET_SKIN_TOOL = {
4
+ name: 'set_skin',
5
+ description: "Apply a named skin to the user's linked runsnative.org tab — the rendering treatment (e.g. wireframe, " +
6
+ 'glassmorphism, crystalline) layered over the active theme, orthogonal to theme, mode, and contrast. Use when ' +
7
+ 'the user asks to try a different look-and-feel treatment. The command is enqueued and the tab applies it ' +
8
+ 'within one poll interval; nothing is saved — fully reversible. Requires an active linked session (call ' +
9
+ 'link_session first).',
10
+ inputSchema: {
11
+ type: 'object',
12
+ properties: {
13
+ skin: { type: 'string', description: 'Skin identifier, e.g. "wireframe" or "glassmorphism".' },
14
+ },
15
+ required: ['skin'],
16
+ },
17
+ // Copresence Protocol §5.1 — session-targeting capability; `enactment` names
18
+ // the worker allow-list entry this tool submits (JUNE-626 / JUNE-739).
19
+ _meta: {
20
+ copresence: {
21
+ target: 'session',
22
+ tier: 'reversible',
23
+ consent: 'implicit',
24
+ entitlement: 'free',
25
+ visibility: 'advertised',
26
+ enactment: 'theme.setSkin',
27
+ },
28
+ },
29
+ };
30
+ export async function handleSetSkin(args) {
31
+ const skin = args['skin'];
32
+ if (typeof skin !== 'string' || !skin.trim()) {
33
+ throw new McpError(ErrorCode.InvalidParams, 'skin must be a non-empty string');
34
+ }
35
+ const pairId = getLinkedPairId();
36
+ if (!pairId) {
37
+ throw new McpError(ErrorCode.InvalidParams, 'No linked session. Call link_session with the pairing code first, or open runsnative.org and start the pairing flow.');
38
+ }
39
+ const res = await postWorkerApi('/tenant/command', {
40
+ pair_id: pairId,
41
+ capability: 'theme',
42
+ method: 'setSkin',
43
+ args: [skin.trim()],
44
+ });
45
+ if (res.status === 404) {
46
+ throw new McpError(ErrorCode.InvalidParams, 'Linked session not found or expired. Re-link with link_session.');
47
+ }
48
+ if (res.status === 422) {
49
+ const body = await res.json().catch(() => ({}));
50
+ throw new McpError(ErrorCode.InvalidParams, body.error ?? 'Command rejected by allow-list.');
51
+ }
52
+ if (res.status === 409) {
53
+ throw new McpError(ErrorCode.InvalidParams, 'No live tab detected. Open runsnative.org, ensure the tab is active, then retry.');
54
+ }
55
+ if (!res.ok) {
56
+ const body = await res.text().catch(() => '');
57
+ throw new McpError(ErrorCode.InternalError, `Worker error ${res.status}: ${body}`);
58
+ }
59
+ return { content: [{ type: 'text', text: `Skin command enqueued: ${skin.trim()}. The tab will apply it within one poll interval.` }] };
60
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runsnative/mcp-server",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist/"