@runsnative/mcp-server 0.7.1 → 0.8.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.
@@ -7,6 +7,22 @@ export function clearLinkedPairId() { _linkedPairId = null; }
7
7
  // Authorization header (KUKAMANGA-wide env-read rule).
8
8
  const BASE_URL = (process.env['RUNSNATIVE_API_URL'] ?? 'https://api.runsnative.org/mcp').trim();
9
9
  const TOKEN = process.env['RUNSNATIVE_TENANT_TOKEN']?.trim();
10
+ // Every worker path below is appended to BASE_URL verbatim ('/content',
11
+ // '/pair/start', …), so BASE_URL must carry the '/mcp' prefix — which the default
12
+ // above does. Pointing RUNSNATIVE_API_URL at the bare origin
13
+ // ('https://api.runsnative.org') builds 'https://api.runsnative.org/content', so
14
+ // every call 404s with a body that names no URL — which reads like a server or
15
+ // auth fault rather than a config typo.
16
+ //
17
+ // JUNE-827 hit exactly this: the documented Claude Desktop config told owners to
18
+ // set the bare origin, so a correctly-installed server failed every write and the
19
+ // error gave no way to see why. Warn at startup instead of 404-ing silently later.
20
+ // stderr only — stdout carries the MCP JSON-RPC frames and must not be polluted.
21
+ if (process.env['RUNSNATIVE_API_URL'] && !/\/mcp\/?$/.test(BASE_URL)) {
22
+ process.stderr.write(`[runsnative-mcp] WARNING: RUNSNATIVE_API_URL is "${BASE_URL}", which does not end in "/mcp".\n` +
23
+ `[runsnative-mcp] Worker requests will resolve to "${BASE_URL}/content" and 404.\n` +
24
+ `[runsnative-mcp] Use "https://api.runsnative.org/mcp", or unset the variable to take the default.\n`);
25
+ }
10
26
  export async function postWorkerApi(path, body) {
11
27
  const headers = { 'Content-Type': 'application/json' };
12
28
  if (TOKEN)
package/dist/server.js CHANGED
@@ -18,6 +18,7 @@ import { GET_EMPHASIS_SCALE_TOOL, handleGetEmphasisScale } from './tools/get-emp
18
18
  import { GET_COMPLETENESS_MAP_TOOL, handleGetCompletenessMap } from './tools/get-completeness-map.js';
19
19
  import { LINK_SESSION_TOOL, handleLinkSession } from './tools/link-session.js';
20
20
  import { SET_SITE_CONTENT_TOOL, handleSetSiteContent } from './tools/set-site-content.js';
21
+ import { PROMOTE_SITE_CONTENT_TOOL, handlePromoteSiteContent } from './tools/promote-site-content.js';
21
22
  import { handleSetTheme } from './tools/set-theme.js';
22
23
  import { handleSetSkin } from './tools/set-skin.js';
23
24
  import { handleSetMode } from './tools/set-mode.js';
@@ -74,6 +75,7 @@ export async function createRunsnativeServer() {
74
75
  GET_COMPLETENESS_MAP_TOOL,
75
76
  LINK_SESSION_TOOL,
76
77
  SET_SITE_CONTENT_TOOL,
78
+ PROMOTE_SITE_CONTENT_TOOL,
77
79
  ...SESSION_ENACT_TOOLS,
78
80
  GET_MARKER_CAPTURE_TOOL,
79
81
  RENDER_MARKER_CAPTURE_TOOL,
@@ -116,6 +118,8 @@ export async function createRunsnativeServer() {
116
118
  return handleLinkSession(request.params.arguments ?? {});
117
119
  case 'set_site_content':
118
120
  return handleSetSiteContent(request.params.arguments ?? {});
121
+ case 'promote_site_content':
122
+ return handlePromoteSiteContent(request.params.arguments ?? {});
119
123
  case 'set_theme':
120
124
  return handleSetTheme(request.params.arguments ?? {});
121
125
  case 'set_skin':
@@ -4,9 +4,12 @@ export const GET_MARKER_CAPTURE_TOOL = {
4
4
  name: 'get_marker_capture',
5
5
  description: 'Fetch pending marker captures from the linked runsnative.org tab — regions the user circled ' +
6
6
  'with the marker overlay. Use when the user says they marked, circled, or pointed at something ' +
7
- 'on the site, or asks "can you see what I selected?". Each capture carries the resolved ' +
8
- 'component-instance address (usable with set_instance_variant), its render inputs, and an ' +
9
- 'optional user note the structural head only (JUNE-623 push→pull inversion). Pixels are NOT ' +
7
+ 'on the site, or asks "can you see what I selected?". Each capture carries the authorable ' +
8
+ 'components inside the circled region: a `container` (where it sits) and a ranked list of ' +
9
+ '`targets` each with a `role`, a content `preview`, and an `instanceId` (usable with ' +
10
+ 'set_instance_variant) — so a request like "change the text here and the icon" maps directly ' +
11
+ 'onto them. `total_in_region`/`truncated` flag when a region held more than the shown targets ' +
12
+ '(ask the user to circle tighter). Structural head only (JUNE-623 push→pull inversion). Pixels are NOT ' +
10
13
  'inlined: when `has_crop` is true and the complaint is visual (not structural), call ' +
11
14
  'fetch_marker_crop with the capture_id to see the region. Single delivery: a capture head is ' +
12
15
  'returned once. Requires an active linked session (call link_session first).',
@@ -61,17 +64,33 @@ export async function handleGetMarkerCapture(_args) {
61
64
  }
62
65
  const content = [];
63
66
  for (const cap of captures) {
64
- content.push({
65
- type: 'text',
66
- text: JSON.stringify({
67
+ // New collection shape when present; legacy single-address otherwise.
68
+ const head = cap.targets
69
+ ? {
67
70
  capture_id: cap.capture_id,
71
+ note: cap.note,
72
+ container: cap.container,
73
+ targets: cap.targets,
74
+ total_in_region: cap.total_in_region,
75
+ truncated: cap.truncated,
76
+ created_at: cap.created_at,
77
+ has_crop: cap.has_crop,
78
+ }
79
+ : {
80
+ capture_id: cap.capture_id,
81
+ note: cap.note,
68
82
  address: cap.address,
69
83
  inputs: cap.inputs,
70
- note: cap.note,
71
84
  created_at: cap.created_at,
72
85
  has_crop: cap.has_crop,
73
- }, null, 2),
74
- });
86
+ };
87
+ content.push({ type: 'text', text: JSON.stringify(head, null, 2) });
88
+ if (cap.truncated && typeof cap.total_in_region === 'number') {
89
+ content.push({
90
+ type: 'text',
91
+ text: `This region holds ${cap.total_in_region} authorable components; the ${cap.targets?.length ?? 0} most relevant are shown. If the user meant a specific one, ask them to circle a tighter region.`,
92
+ });
93
+ }
75
94
  }
76
95
  if (data.truncated) {
77
96
  content.push({
@@ -0,0 +1,58 @@
1
+ import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
2
+ import { postWorkerApi } from '../bridge-client.js';
3
+ // Sibling of set_site_content (Authoring continuum A, JUNE-866): the publish
4
+ // verb. Copies the caller's current PREVIEW version of a section into
5
+ // PUBLISHED, unreviewed, no second confirm (change-authoring blast-radius
6
+ // continuum v1 §12.2). Ordinary MCP tool — does not target a bound live
7
+ // session, so no _meta.copresence, same as set_site_content.
8
+ export const PROMOTE_SITE_CONTENT_TOOL = {
9
+ name: 'promote_site_content',
10
+ description: "Make the user's current preview edit for a site content section LIVE. Copies the section's " +
11
+ 'current preview content to production (published) as-is — it does not take new content, only a ' +
12
+ 'section key. Use this when the user says the preview looks right and they want to ship it ' +
13
+ '(e.g. "publish that" / "make it live"). There is no undo prompt: this action ships immediately. ' +
14
+ 'Requires an authenticated RunsNative token whose role may edit this site.',
15
+ inputSchema: {
16
+ type: 'object',
17
+ properties: {
18
+ section_key: {
19
+ type: 'string',
20
+ description: "The section to publish, page-namespaced, e.g. 'index:les-heures'.",
21
+ },
22
+ },
23
+ required: ['section_key'],
24
+ },
25
+ };
26
+ export async function handlePromoteSiteContent(args) {
27
+ const sectionKey = args['section_key'];
28
+ if (typeof sectionKey !== 'string' || !sectionKey.trim()) {
29
+ throw new McpError(ErrorCode.InvalidParams, 'section_key must be a non-empty string');
30
+ }
31
+ const res = await postWorkerApi('/content/promote', { section_key: sectionKey.trim() });
32
+ if (res.status === 401) {
33
+ throw new McpError(ErrorCode.InvalidParams, 'Not authenticated. Set RUNSNATIVE_TENANT_TOKEN to a token that may edit this site.');
34
+ }
35
+ if (res.status === 403) {
36
+ throw new McpError(ErrorCode.InvalidParams, 'Your role does not permit publishing this site.');
37
+ }
38
+ if (res.status === 404) {
39
+ throw new McpError(ErrorCode.InvalidParams, 'No preview content for that section — nothing to publish.');
40
+ }
41
+ if (res.status === 400) {
42
+ const body = (await res.json().catch(() => ({})));
43
+ throw new McpError(ErrorCode.InvalidParams, body.error ?? 'Section rejected by the server.');
44
+ }
45
+ if (!res.ok) {
46
+ const body = await res.text().catch(() => '');
47
+ throw new McpError(ErrorCode.InternalError, `Worker error ${res.status}: ${body}`);
48
+ }
49
+ const body = (await res.json());
50
+ return {
51
+ content: [
52
+ {
53
+ type: 'text',
54
+ text: `Published "${body.section_key}" (version ${body.version}). The production site now shows this content.`,
55
+ },
56
+ ],
57
+ };
58
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runsnative/mcp-server",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist/"