@runsnative/mcp-server 0.5.1 → 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.
- package/README.md +45 -1
- package/dist/content.js +58 -17
- package/dist/server.js +46 -9
- package/dist/surface/agent-surface.js +183 -0
- package/dist/surface/engine-gateway-transport.js +82 -0
- package/dist/surface/gathering-card.js +40 -1
- package/dist/surface/marker-card.js +245 -0
- package/dist/tools/apply-inferred-theme.js +4 -4
- package/dist/tools/fetch-marker-crop.js +67 -0
- package/dist/tools/get-marker-capture.js +11 -19
- package/dist/tools/navigate.js +4 -4
- package/dist/tools/render-marker-capture.js +83 -0
- package/dist/tools/render-surface.js +204 -0
- package/dist/tools/search-docs.js +1 -1
- package/dist/tools/session-tools.js +38 -0
- package/dist/tools/set-contrast.js +59 -0
- package/dist/tools/set-instance-variant.js +4 -4
- package/dist/tools/set-mode.js +59 -0
- package/dist/tools/set-site-content.js +69 -0
- package/dist/tools/set-skin.js +60 -0
- package/dist/tools/set-theme.js +4 -4
- package/dist/tools/spotlight.js +85 -0
- package/package.json +1 -1
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The marker-card MCP App shell (JUNE-623) — the lasso round-trip finale.
|
|
3
|
+
*
|
|
4
|
+
* Fourth consumer of the JUNE-618 packaging seam (see gathering-card.ts for
|
|
5
|
+
* the first). Static shell: no per-call data is baked in. The host delivers
|
|
6
|
+
* the tool result (the capture's structural head) via the MCP Apps
|
|
7
|
+
* tool-result notification; the app script below renders the card
|
|
8
|
+
* client-side with DOM APIs (textContent only — a user-authored `note` can
|
|
9
|
+
* never parse as markup).
|
|
10
|
+
*
|
|
11
|
+
* Push→pull inversion (JUNE-623 comment 1, founder-normative): the pixel
|
|
12
|
+
* crop is NOT inlined in the tool result. The thumbnail slot starts empty;
|
|
13
|
+
* "Show the pixels" fetches it on demand via fetch_marker_crop.
|
|
14
|
+
*/
|
|
15
|
+
import { packageSurface } from './package-surface.js';
|
|
16
|
+
export const MARKER_CARD_URI = 'ui://runsnative/marker-card.html';
|
|
17
|
+
const CARD_CSS = `
|
|
18
|
+
@layer rn-shell, primitives, semantics, themes;
|
|
19
|
+
@layer rn-shell {
|
|
20
|
+
:root {
|
|
21
|
+
--run-color-text-primary: #1e293b;
|
|
22
|
+
--run-color-text-secondary: #64748b;
|
|
23
|
+
--run-color-surface-default: #ffffff;
|
|
24
|
+
--run-color-surface-subtle: #f8fafc;
|
|
25
|
+
--run-color-surface-canvas: transparent;
|
|
26
|
+
--run-color-border-default: #e2e8f0;
|
|
27
|
+
--run-color-focus-ring: #2563eb;
|
|
28
|
+
--run-font-size-s: 14px;
|
|
29
|
+
--run-font-size-m: 16px;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
33
|
+
body {
|
|
34
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
35
|
+
color: var(--run-color-text-primary);
|
|
36
|
+
background: var(--run-color-surface-canvas);
|
|
37
|
+
padding: 12px;
|
|
38
|
+
}
|
|
39
|
+
.marker-card {
|
|
40
|
+
max-width: 440px;
|
|
41
|
+
border: 1px solid var(--run-color-border-default);
|
|
42
|
+
border-radius: 12px;
|
|
43
|
+
background: var(--run-color-surface-default);
|
|
44
|
+
padding: 20px;
|
|
45
|
+
display: none;
|
|
46
|
+
flex-direction: column;
|
|
47
|
+
gap: 10px;
|
|
48
|
+
}
|
|
49
|
+
.marker-card.is-ready { display: flex; }
|
|
50
|
+
.card-inputs {
|
|
51
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
52
|
+
font-size: 12px;
|
|
53
|
+
color: var(--run-color-text-secondary);
|
|
54
|
+
background: var(--run-color-surface-subtle);
|
|
55
|
+
border-radius: 6px;
|
|
56
|
+
padding: 8px 10px;
|
|
57
|
+
overflow-wrap: anywhere;
|
|
58
|
+
}
|
|
59
|
+
.card-thumb {
|
|
60
|
+
display: none;
|
|
61
|
+
max-width: 100%;
|
|
62
|
+
border-radius: 8px;
|
|
63
|
+
border: 1px solid var(--run-color-border-default);
|
|
64
|
+
}
|
|
65
|
+
.card-thumb.is-shown { display: block; }
|
|
66
|
+
.card-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 4px; }
|
|
67
|
+
.card-change-row { display: none; align-items: center; gap: 8px; }
|
|
68
|
+
.card-change-row.is-shown { display: flex; }
|
|
69
|
+
.card-status { font-size: 12px; color: var(--run-color-text-secondary); min-height: 1.2em; }
|
|
70
|
+
#placeholder { font-size: 13px; color: var(--run-color-text-secondary); padding: 8px 2px; }
|
|
71
|
+
`;
|
|
72
|
+
const CARD_BODY = `
|
|
73
|
+
<div id="placeholder">Waiting for the region you circled…</div>
|
|
74
|
+
<article class="marker-card" id="card">
|
|
75
|
+
<run-text variant="label" as="span" size="sm" color="secondary">You circled this</run-text>
|
|
76
|
+
<run-text id="card-type" variant="title" as="h3" size="md"></run-text>
|
|
77
|
+
<run-text id="card-note" variant="body" as="p" size="sm" color="secondary"></run-text>
|
|
78
|
+
<pre class="card-inputs" id="card-inputs"></pre>
|
|
79
|
+
<img class="card-thumb" id="card-thumb" alt="The region you circled" />
|
|
80
|
+
<div class="card-actions">
|
|
81
|
+
<run-button id="spotlight-btn" variant="primary" size="sm">Show me where this is</run-button>
|
|
82
|
+
<run-button id="whatis-btn" variant="secondary" size="sm">What is this?</run-button>
|
|
83
|
+
<run-button id="pixels-btn" variant="secondary" size="sm">Show the pixels</run-button>
|
|
84
|
+
</div>
|
|
85
|
+
<div class="card-change-row" id="change-row">
|
|
86
|
+
<select id="variant-select">
|
|
87
|
+
<option value="primary">primary</option>
|
|
88
|
+
<option value="secondary">secondary</option>
|
|
89
|
+
<option value="outline">outline</option>
|
|
90
|
+
<option value="ghost">ghost</option>
|
|
91
|
+
<option value="danger">danger</option>
|
|
92
|
+
<option value="link">link</option>
|
|
93
|
+
</select>
|
|
94
|
+
<run-button id="change-btn" variant="outline" size="sm">Change it</run-button>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="card-status" id="card-status"></div>
|
|
97
|
+
</article>
|
|
98
|
+
`;
|
|
99
|
+
// Static, data-free by design — see gathering-card.ts for the rationale this
|
|
100
|
+
// mirrors: everything user-authored arrives at runtime via the tool-result
|
|
101
|
+
// notification and is assigned with textContent, never interpolated here.
|
|
102
|
+
const CARD_APP_SCRIPT = `
|
|
103
|
+
const { App } = globalThis.__MCP_APP_SDK__;
|
|
104
|
+
|
|
105
|
+
let currentCapture = null;
|
|
106
|
+
|
|
107
|
+
function setText(id, value) {
|
|
108
|
+
document.getElementById(id).textContent = value == null ? '' : String(value);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function render(payload) {
|
|
112
|
+
const capture = payload && payload.capture;
|
|
113
|
+
if (!capture) return;
|
|
114
|
+
currentCapture = capture;
|
|
115
|
+
const card = document.getElementById('card');
|
|
116
|
+
const address = capture.address || {};
|
|
117
|
+
|
|
118
|
+
setText('card-type', address.type || 'Unknown component');
|
|
119
|
+
setText('card-note', capture.note || '');
|
|
120
|
+
document.getElementById('card-inputs').textContent =
|
|
121
|
+
JSON.stringify(capture.inputs || {}, null, 2);
|
|
122
|
+
|
|
123
|
+
// "Change it" is JUNE-512's write surface — run-button only. Omit the
|
|
124
|
+
// control entirely for other types rather than shipping a button that 422s.
|
|
125
|
+
const changeRow = document.getElementById('change-row');
|
|
126
|
+
changeRow.classList.toggle('is-shown', address.type === 'run-button');
|
|
127
|
+
|
|
128
|
+
document.getElementById('pixels-btn').disabled = !capture.has_crop;
|
|
129
|
+
|
|
130
|
+
document.getElementById('placeholder').style.display = 'none';
|
|
131
|
+
card.classList.add('is-ready');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const app = new App({ name: 'runsnative-marker-card', version: '1.0.0' }, {});
|
|
135
|
+
|
|
136
|
+
// Register before connect() so no notification is missed.
|
|
137
|
+
app.ontoolresult = (params) => {
|
|
138
|
+
if (params && params.isError) {
|
|
139
|
+
setText('placeholder', 'Could not load the marker capture — see the conversation for details.');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
render(params && params.structuredContent);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
document.getElementById('spotlight-btn').addEventListener('click', async () => {
|
|
146
|
+
const status = document.getElementById('card-status');
|
|
147
|
+
const address = currentCapture && currentCapture.address;
|
|
148
|
+
const instanceId = address && address.instanceId;
|
|
149
|
+
if (!instanceId) {
|
|
150
|
+
status.textContent = 'This capture has no addressable instance.';
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
status.textContent = 'Pointing your tab at the element you circled…';
|
|
154
|
+
try {
|
|
155
|
+
const result = await app.callServerTool({
|
|
156
|
+
name: 'spotlight',
|
|
157
|
+
arguments: { target: 'instance:' + instanceId, message: "Here's the element you circled." },
|
|
158
|
+
});
|
|
159
|
+
status.textContent = result && result.isError
|
|
160
|
+
? 'No linked tab — open runsnative.org to see it live.'
|
|
161
|
+
: 'Watch your linked tab — the ring is landing on the element.';
|
|
162
|
+
} catch (e) {
|
|
163
|
+
status.textContent = 'No linked tab — open runsnative.org to see it live.';
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
document.getElementById('whatis-btn').addEventListener('click', async () => {
|
|
168
|
+
const status = document.getElementById('card-status');
|
|
169
|
+
const address = currentCapture && currentCapture.address;
|
|
170
|
+
if (!address || !address.type) {
|
|
171
|
+
status.textContent = 'No component type on this capture.';
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
status.textContent = 'Asking about ' + address.type + '…';
|
|
175
|
+
try {
|
|
176
|
+
const result = await app.callServerTool({ name: 'get_component', arguments: { name: address.type } });
|
|
177
|
+
status.textContent = result && result.isError
|
|
178
|
+
? 'Could not look up that component.'
|
|
179
|
+
: 'See the conversation for ' + address.type + \"'s contract.\";
|
|
180
|
+
} catch (e) {
|
|
181
|
+
status.textContent = 'Could not look up that component.';
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
document.getElementById('pixels-btn').addEventListener('click', async () => {
|
|
186
|
+
const status = document.getElementById('card-status');
|
|
187
|
+
if (!currentCapture || !currentCapture.has_crop) {
|
|
188
|
+
status.textContent = 'No pixels were captured for this region.';
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
status.textContent = 'Fetching the pixels…';
|
|
192
|
+
try {
|
|
193
|
+
const result = await app.callServerTool({
|
|
194
|
+
name: 'fetch_marker_crop',
|
|
195
|
+
arguments: { capture_id: currentCapture.capture_id },
|
|
196
|
+
});
|
|
197
|
+
const block = result && Array.isArray(result.content)
|
|
198
|
+
? result.content.find((c) => c.type === 'image')
|
|
199
|
+
: null;
|
|
200
|
+
if (result && result.isError || !block) {
|
|
201
|
+
status.textContent = 'Could not fetch the pixels — they may have expired.';
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const thumb = document.getElementById('card-thumb');
|
|
205
|
+
thumb.src = 'data:' + (block.mimeType || 'image/png') + ';base64,' + block.data;
|
|
206
|
+
thumb.classList.add('is-shown');
|
|
207
|
+
status.textContent = '';
|
|
208
|
+
} catch (e) {
|
|
209
|
+
status.textContent = 'Could not fetch the pixels — they may have expired.';
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
document.getElementById('change-btn').addEventListener('click', async () => {
|
|
214
|
+
const status = document.getElementById('card-status');
|
|
215
|
+
const address = currentCapture && currentCapture.address;
|
|
216
|
+
const instanceId = address && address.instanceId;
|
|
217
|
+
if (!instanceId) {
|
|
218
|
+
status.textContent = 'This capture has no addressable instance.';
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const variant = document.getElementById('variant-select').value;
|
|
222
|
+
status.textContent = 'Sending ' + variant + ' to your tab…';
|
|
223
|
+
try {
|
|
224
|
+
const result = await app.callServerTool({
|
|
225
|
+
name: 'set_instance_variant',
|
|
226
|
+
arguments: { instance_id: instanceId, variant: variant },
|
|
227
|
+
});
|
|
228
|
+
status.textContent = result && result.isError
|
|
229
|
+
? 'No linked tab — open runsnative.org to try it live.'
|
|
230
|
+
: 'Watch your linked tab — the button is switching to ' + variant + '.';
|
|
231
|
+
} catch (e) {
|
|
232
|
+
status.textContent = 'No linked tab — open runsnative.org to try it live.';
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
await app.connect();
|
|
237
|
+
`;
|
|
238
|
+
export function buildMarkerCardHtml(assets) {
|
|
239
|
+
return packageSurface({
|
|
240
|
+
title: 'The region you circled',
|
|
241
|
+
bodyHtml: CARD_BODY,
|
|
242
|
+
css: CARD_CSS,
|
|
243
|
+
appScript: CARD_APP_SCRIPT,
|
|
244
|
+
}, assets);
|
|
245
|
+
}
|
|
@@ -19,11 +19,11 @@ export const APPLY_INFERRED_THEME_TOOL = {
|
|
|
19
19
|
},
|
|
20
20
|
required: ['brief'],
|
|
21
21
|
},
|
|
22
|
-
//
|
|
23
|
-
// the bridge (JUNE-650). `enactment` names the worker
|
|
24
|
-
// command leg submits after the paid inference leg succeeds.
|
|
22
|
+
// Copresence Protocol §5.1 — session-targeting; the first entitlement:'paid'
|
|
23
|
+
// capability on the bridge (JUNE-650). `enactment` names the worker
|
|
24
|
+
// allow-list entry the command leg submits after the paid inference leg succeeds.
|
|
25
25
|
_meta: {
|
|
26
|
-
|
|
26
|
+
copresence: {
|
|
27
27
|
target: 'session',
|
|
28
28
|
tier: 'reversible',
|
|
29
29
|
consent: 'implicit',
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { getWorkerApi, getLinkedPairId } from '../bridge-client.js';
|
|
3
|
+
export const FETCH_MARKER_CROP_TOOL = {
|
|
4
|
+
name: 'fetch_marker_crop',
|
|
5
|
+
description: 'Fetch the pixel crop for a marker capture, on demand (JUNE-623 push→pull inversion). ' +
|
|
6
|
+
'render_marker_capture and get_marker_capture deliver the structural head only — call this when the ' +
|
|
7
|
+
'user\'s complaint is visual (a gap, alignment, or render fidelity issue that a component-instance ' +
|
|
8
|
+
'address alone cannot answer). Requires an active linked session; the crop must still be within its ' +
|
|
9
|
+
'capture window (~15 minutes) or this returns not-found.',
|
|
10
|
+
inputSchema: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
capture_id: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
description: 'The capture_id from a marker-capture head (get_marker_capture or render_marker_capture).',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
required: ['capture_id'],
|
|
19
|
+
},
|
|
20
|
+
// Copresence Protocol §5.1 (copresence-protocol-v0.1, mukadra repo) — a
|
|
21
|
+
// session-targeting READ leg like get_marker_capture: it drains a stored
|
|
22
|
+
// resource rather than enacting a command, so no allow-list `enactment`.
|
|
23
|
+
_meta: {
|
|
24
|
+
copresence: {
|
|
25
|
+
target: 'session',
|
|
26
|
+
tier: 'reversible',
|
|
27
|
+
consent: 'implicit',
|
|
28
|
+
entitlement: 'free',
|
|
29
|
+
visibility: 'advertised',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
export async function handleFetchMarkerCrop(args) {
|
|
34
|
+
const captureId = args['capture_id'];
|
|
35
|
+
if (typeof captureId !== 'string' || !captureId.trim()) {
|
|
36
|
+
throw new McpError(ErrorCode.InvalidParams, 'capture_id must be a non-empty string');
|
|
37
|
+
}
|
|
38
|
+
const pairId = getLinkedPairId();
|
|
39
|
+
if (!pairId) {
|
|
40
|
+
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.');
|
|
41
|
+
}
|
|
42
|
+
const res = await getWorkerApi(`/session/${pairId}/captures/${encodeURIComponent(captureId.trim())}/crop`);
|
|
43
|
+
if (res.status === 404) {
|
|
44
|
+
throw new McpError(ErrorCode.InvalidParams, 'Crop not found — it may have expired, or the linked session was re-linked. Re-circle the region if you still need it.');
|
|
45
|
+
}
|
|
46
|
+
if (res.status === 410) {
|
|
47
|
+
throw new McpError(ErrorCode.InvalidParams, 'Linked session revoked or expired. Re-link with link_session.');
|
|
48
|
+
}
|
|
49
|
+
if (res.status === 409) {
|
|
50
|
+
throw new McpError(ErrorCode.InvalidParams, 'No live tab detected. Open runsnative.org, ensure the tab is active, then retry.');
|
|
51
|
+
}
|
|
52
|
+
if (!res.ok) {
|
|
53
|
+
const body = await res.text().catch(() => '');
|
|
54
|
+
throw new McpError(ErrorCode.InternalError, `Worker error ${res.status}: ${body}`);
|
|
55
|
+
}
|
|
56
|
+
const buffer = await res.arrayBuffer();
|
|
57
|
+
const bytes = new Uint8Array(buffer);
|
|
58
|
+
let binary = '';
|
|
59
|
+
const CHUNK = 0x8000;
|
|
60
|
+
for (let i = 0; i < bytes.length; i += CHUNK) {
|
|
61
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
|
62
|
+
}
|
|
63
|
+
const b64 = btoa(binary);
|
|
64
|
+
return {
|
|
65
|
+
content: [{ type: 'image', data: b64, mimeType: 'image/png' }],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
2
2
|
import { getWorkerApi, getLinkedPairId } from '../bridge-client.js';
|
|
3
|
-
const CROP_PREFIX = 'data:image/png;base64,';
|
|
4
3
|
export const GET_MARKER_CAPTURE_TOOL = {
|
|
5
4
|
name: 'get_marker_capture',
|
|
6
5
|
description: 'Fetch pending marker captures from the linked runsnative.org tab — regions the user circled ' +
|
|
7
6
|
'with the marker overlay. Use when the user says they marked, circled, or pointed at something ' +
|
|
8
7
|
'on the site, or asks "can you see what I selected?". Each capture carries the resolved ' +
|
|
9
|
-
'component-instance address (usable with set_instance_variant), its render inputs, an
|
|
10
|
-
'user note
|
|
11
|
-
'
|
|
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 ' +
|
|
10
|
+
'inlined: when `has_crop` is true and the complaint is visual (not structural), call ' +
|
|
11
|
+
'fetch_marker_crop with the capture_id to see the region. Single delivery: a capture head is ' +
|
|
12
|
+
'returned once. Requires an active linked session (call link_session first).',
|
|
12
13
|
inputSchema: {
|
|
13
14
|
type: 'object',
|
|
14
15
|
properties: {},
|
|
15
16
|
},
|
|
16
|
-
//
|
|
17
|
-
// but the READ leg of the bridge: it drains the
|
|
18
|
-
// rather than enacting a command, so there is no
|
|
19
|
-
// (the field is optional per §5.1).
|
|
17
|
+
// Copresence Protocol §5.1 (copresence-protocol-v0.1, mukadra repo) —
|
|
18
|
+
// session-targeting, but the READ leg of the bridge: it drains the
|
|
19
|
+
// session's capture queue rather than enacting a command, so there is no
|
|
20
|
+
// allow-list `enactment` (the field is optional per §5.1).
|
|
20
21
|
_meta: {
|
|
21
|
-
|
|
22
|
+
copresence: {
|
|
22
23
|
target: 'session',
|
|
23
24
|
tier: 'reversible',
|
|
24
25
|
consent: 'implicit',
|
|
@@ -68,18 +69,9 @@ export async function handleGetMarkerCapture(_args) {
|
|
|
68
69
|
inputs: cap.inputs,
|
|
69
70
|
note: cap.note,
|
|
70
71
|
created_at: cap.created_at,
|
|
72
|
+
has_crop: cap.has_crop,
|
|
71
73
|
}, null, 2),
|
|
72
74
|
});
|
|
73
|
-
// The pixel crop is co-equal with the structural address (design doc
|
|
74
|
-
// §3.4) — deliver it as an image block so the agent SEES the region,
|
|
75
|
-
// not a reference to it.
|
|
76
|
-
if (cap.crop && cap.crop.startsWith(CROP_PREFIX)) {
|
|
77
|
-
content.push({
|
|
78
|
-
type: 'image',
|
|
79
|
-
data: cap.crop.slice(CROP_PREFIX.length),
|
|
80
|
-
mimeType: 'image/png',
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
75
|
}
|
|
84
76
|
if (data.truncated) {
|
|
85
77
|
content.push({
|
package/dist/tools/navigate.js
CHANGED
|
@@ -14,11 +14,11 @@ export const NAVIGATE_TOOL = {
|
|
|
14
14
|
},
|
|
15
15
|
required: ['path'],
|
|
16
16
|
},
|
|
17
|
-
//
|
|
18
|
-
// session-targeting capability. `enactment` names the worker
|
|
19
|
-
// entry this tool submits (JUNE-626).
|
|
17
|
+
// Copresence Protocol §5.1 (copresence-protocol-v0.1, mukadra repo) — marks
|
|
18
|
+
// this as a session-targeting capability. `enactment` names the worker
|
|
19
|
+
// allow-list entry this tool submits (JUNE-626).
|
|
20
20
|
_meta: {
|
|
21
|
-
|
|
21
|
+
copresence: {
|
|
22
22
|
target: 'session',
|
|
23
23
|
tier: 'reversible',
|
|
24
24
|
consent: 'implicit',
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { getWorkerApi, getLinkedPairId } from '../bridge-client.js';
|
|
3
|
+
import { componentsBundle, iframeRuntime } from '../surface/assets.js';
|
|
4
|
+
import { buildMarkerCardHtml, MARKER_CARD_URI } from '../surface/marker-card.js';
|
|
5
|
+
import { SURFACE_MIME_TYPE } from '../surface/package-surface.js';
|
|
6
|
+
export const RENDER_MARKER_CAPTURE_TOOL = {
|
|
7
|
+
name: 'render_marker_capture',
|
|
8
|
+
description: "Render the user's most recent marker capture — a region they circled with the marker overlay on " +
|
|
9
|
+
'the linked runsnative.org tab — as a live MCP App card in this conversation (JUNE-623). The card is ' +
|
|
10
|
+
'a HANDLE, not a picture: it carries live actions targeting the captured instance ("Show me where " +' +
|
|
11
|
+
'"this is", "Change it", "What is this?"), plus an on-demand pixel fetch. Use when the user says they ' +
|
|
12
|
+
'circled or marked something and you want to hand them (and yourself) a live surface instead of raw ' +
|
|
13
|
+
'JSON. Requires an active linked session (call link_session first).',
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {},
|
|
17
|
+
required: [],
|
|
18
|
+
},
|
|
19
|
+
_meta: {
|
|
20
|
+
ui: { resourceUri: MARKER_CARD_URI },
|
|
21
|
+
// Legacy key for hosts predating _meta.ui (ext-apps compatibility shim).
|
|
22
|
+
'ui/resourceUri': MARKER_CARD_URI,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
export const MARKER_CARD_RESOURCE = {
|
|
26
|
+
uri: MARKER_CARD_URI,
|
|
27
|
+
name: 'Marker Capture Card',
|
|
28
|
+
description: 'Self-contained MCP App shell that renders a marker capture as a live RunsNative card with ' +
|
|
29
|
+
'spotlight/change/introspect actions. The pixel crop is fetched on demand, never inlined.',
|
|
30
|
+
mimeType: SURFACE_MIME_TYPE,
|
|
31
|
+
};
|
|
32
|
+
export async function readMarkerCardResource() {
|
|
33
|
+
const html = buildMarkerCardHtml({
|
|
34
|
+
componentsBundle: componentsBundle(),
|
|
35
|
+
iframeRuntime: iframeRuntime(),
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
contents: [{ uri: MARKER_CARD_URI, mimeType: SURFACE_MIME_TYPE, text: html }],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export async function handleRenderMarkerCapture() {
|
|
42
|
+
const pairId = getLinkedPairId();
|
|
43
|
+
if (!pairId) {
|
|
44
|
+
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.');
|
|
45
|
+
}
|
|
46
|
+
const res = await getWorkerApi(`/session/${pairId}/captures`);
|
|
47
|
+
if (res.status === 404) {
|
|
48
|
+
throw new McpError(ErrorCode.InvalidParams, 'Linked session not found or expired. Re-link with link_session.');
|
|
49
|
+
}
|
|
50
|
+
if (res.status === 410) {
|
|
51
|
+
throw new McpError(ErrorCode.InvalidParams, 'Linked session revoked or expired. Re-link with link_session.');
|
|
52
|
+
}
|
|
53
|
+
if (res.status === 409) {
|
|
54
|
+
throw new McpError(ErrorCode.InvalidParams, 'No live tab detected. Open runsnative.org, ensure the tab is active, then retry.');
|
|
55
|
+
}
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
const body = await res.text().catch(() => '');
|
|
58
|
+
throw new McpError(ErrorCode.InternalError, `Worker error ${res.status}: ${body}`);
|
|
59
|
+
}
|
|
60
|
+
const data = (await res.json());
|
|
61
|
+
const captures = data.captures ?? [];
|
|
62
|
+
if (captures.length === 0) {
|
|
63
|
+
return {
|
|
64
|
+
content: [
|
|
65
|
+
{
|
|
66
|
+
type: 'text',
|
|
67
|
+
text: 'No pending marker captures. Ask the user to arm the marker overlay and circle a region on the linked tab.',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// Head endpoint returns oldest-first — the most recent pending capture is last.
|
|
73
|
+
const capture = captures[captures.length - 1];
|
|
74
|
+
return {
|
|
75
|
+
content: [
|
|
76
|
+
{
|
|
77
|
+
type: 'text',
|
|
78
|
+
text: 'Rendered the region the user circled as a live card in the conversation.',
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
structuredContent: { capture },
|
|
82
|
+
};
|
|
83
|
+
}
|