@octane-xplat/mcp 0.1.0 → 0.15.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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +338 -1
  3. package/package.json +37 -4
  4. package/src/actions/bridge.mjs +2 -0
  5. package/src/contracts/actions.d.ts +209 -0
  6. package/src/contracts/actions.mjs +3 -0
  7. package/src/contracts/boundaries.d.ts +6 -0
  8. package/src/contracts/boundaries.mjs +3 -0
  9. package/src/contracts/capabilities.d.ts +106 -0
  10. package/src/contracts/capabilities.mjs +3 -0
  11. package/src/contracts/code.d.ts +134 -0
  12. package/src/contracts/code.mjs +3 -0
  13. package/src/contracts/errors.d.ts +75 -0
  14. package/src/contracts/errors.mjs +3 -0
  15. package/src/contracts/evidence.d.ts +115 -0
  16. package/src/contracts/evidence.mjs +3 -0
  17. package/src/contracts/identity.d.ts +127 -0
  18. package/src/contracts/identity.mjs +3 -0
  19. package/src/contracts/index.d.ts +11 -0
  20. package/src/contracts/index.mjs +3 -0
  21. package/src/contracts/inspection.d.ts +45 -0
  22. package/src/contracts/inspection.mjs +3 -0
  23. package/src/contracts/policy.d.ts +86 -0
  24. package/src/contracts/policy.mjs +3 -0
  25. package/src/contracts/sessions.d.ts +160 -0
  26. package/src/contracts/sessions.mjs +3 -0
  27. package/src/contracts/status.d.ts +40 -0
  28. package/src/contracts/status.mjs +3 -0
  29. package/src/discovery.mjs +2 -0
  30. package/src/docs.mjs +84 -0
  31. package/src/inspection/service.mjs +5 -0
  32. package/src/inspection/tools.mjs +53 -0
  33. package/src/runtime/logs.mjs +2 -0
  34. package/src/runtime/process.mjs +2 -0
  35. package/src/runtime/service.mjs +2 -0
  36. package/src/runtime/targets.mjs +2 -0
  37. package/src/runtime/tools.mjs +126 -0
  38. package/src/server.mjs +565 -0
  39. package/src/status.mjs +2 -0
  40. package/src/xplat-mcp.mjs +85 -0
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Permission/policy contract. Workspace scope and ordinary runtime actions
3
+ * reuse the existing task authorization — policy is established once, not
4
+ * re-requested per tool call. Grants are scoped, revocable, optionally
5
+ * expiring, and checked at action time. `visual-capture` is never part of
6
+ * any baseline: the screenshot capability can exist while every actual
7
+ * capture still requires explicit per-task developer approval.
8
+ */
9
+
10
+ import type { ContractError } from './errors'
11
+
12
+ export const PERMISSION_SCOPES: readonly PermissionScope[]
13
+
14
+ export type PermissionScope =
15
+ /** Read the selected workspace: manifests, config, source inventory. */
16
+ | 'workspace'
17
+ /** Build, launch, attach, and interact with development sessions. */
18
+ | 'runtime-actions'
19
+ /** Screenshot capture and any visual inspection of captured pixels. Never granted by task authorization alone. */
20
+ | 'visual-capture'
21
+ /** Persist evidence artifacts beyond the MCP session. */
22
+ | 'evidence-retain'
23
+ /** Effects outside the app itself (external services, destructive resets). Never implied by runtime-actions. */
24
+ | 'external-effects'
25
+
26
+ export type GrantSource =
27
+ /** The developer authorized this task; covers workspace + runtime-actions only. */
28
+ | 'task-authorization'
29
+ /** An explicit developer approval for this task (e.g. via client elicitation). */
30
+ | 'developer'
31
+ /** A server startup flag/environment setting the developer configured. */
32
+ | 'server-config'
33
+
34
+ export type GrantState = 'granted' | 'denied' | 'required'
35
+
36
+ export interface PermissionGrant {
37
+ scope: PermissionScope
38
+ state: GrantState
39
+ source?: GrantSource
40
+ grantedAt?: string
41
+ /** ISO timestamp after which the grant no longer applies; absent = session-bound. */
42
+ expiresAt?: string
43
+ /** False only for server-internal baseline entries; everything the developer grants is revocable. */
44
+ revocable: boolean
45
+ note?: string
46
+ }
47
+
48
+ export type RequireResult =
49
+ | { granted: true; grant: PermissionGrant }
50
+ | { granted: false; error: ContractError }
51
+
52
+ /**
53
+ * The channel that asks the developer for a grant — wired to the client's
54
+ * elicitation support when available. Returns the resulting state.
55
+ */
56
+ export type GrantElicitor = (
57
+ scope: PermissionScope,
58
+ reason: string,
59
+ ) => Promise<'granted' | 'denied'> | 'granted' | 'denied'
60
+
61
+ export interface PolicyStoreOptions {
62
+ /** The developer authorized this task — grants workspace + runtime-actions for the session. */
63
+ taskAuthorization?: boolean
64
+ /** Extra grants from server configuration (env/flags), e.g. a pre-approved visual-capture. */
65
+ grants?: PermissionGrant[]
66
+ /** Optional elicitation channel used by `requestGrant`. */
67
+ elicit?: GrantElicitor
68
+ }
69
+
70
+ export interface PolicyStore {
71
+ /** Synchronous check used at action time. Expired grants are denied. */
72
+ require(scope: PermissionScope): RequireResult
73
+ /**
74
+ * Resolve a grant, asking the developer through the elicitation channel when
75
+ * one exists. Without a channel, returns `required` state plus the recovery
76
+ * text telling the developer how to grant it.
77
+ */
78
+ requestGrant(scope: PermissionScope, reason: string): Promise<RequireResult>
79
+ grant(scope: PermissionScope, grant?: Partial<PermissionGrant>): PermissionGrant
80
+ revoke(scope: PermissionScope): boolean
81
+ list(): PermissionGrant[]
82
+ /** Late-bind the elicitation channel (it needs a connected client). */
83
+ setElicitor(elicit: GrantElicitor): void
84
+ }
85
+
86
+ export declare function createPolicyStore(options?: PolicyStoreOptions): PolicyStore
@@ -0,0 +1,3 @@
1
+ // Runtime contract moved to @octane-xplat/cli (shared agent service layer).
2
+ // This shim preserves the @octane-xplat/mcp ./contracts export surface.
3
+ export * from '@octane-xplat/cli/mcp/contracts/policy'
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Session, job, and ownership contract.
3
+ *
4
+ * A session binds one app to one target. Sessions are either `owned` (this
5
+ * server started them — ending releases their resources) or `attached`
6
+ * (a development session the user already had running — ending detaches and
7
+ * preserves it). Restarting always establishes a new session identity; tools
8
+ * refuse operations against stopped or disconnected sessions.
9
+ *
10
+ * Jobs are bounded units of work (build, launch, dev-action, …) whose status
11
+ * is queryable without holding a tool call open. Repeating a request with the
12
+ * same `requestKey` returns the live job instead of duplicating it.
13
+ * Cancellation is cooperative: `cancelling` and `cancelled` are distinct
14
+ * states, and cancelling a job only signals that job's own operation — it
15
+ * must never kill an unrelated app.
16
+ */
17
+
18
+ import type { ContractError } from './errors'
19
+ import type { Platform } from './identity'
20
+
21
+ export const SESSION_STATES: readonly SessionState[]
22
+ export const JOB_STATES: readonly JobState[]
23
+ export const JOB_TERMINAL_STATES: readonly JobState[]
24
+
25
+ export type SessionState =
26
+ /** Bound but not yet ready for observation/interaction. */
27
+ | 'starting'
28
+ /** Ready — inspection, logs, and authorized interactions may proceed. */
29
+ | 'ready'
30
+ /** Lost contact; last-known data is historical evidence, never current state. */
31
+ | 'disconnected'
32
+ /** Ended — identity is stale; a new run is a new sessionId. */
33
+ | 'stopped'
34
+
35
+ export type SessionOwnership =
36
+ /** Started by this server; end releases owned resources. */
37
+ | 'owned'
38
+ /** Pre-existing user session; end detaches and preserves it. */
39
+ | 'attached'
40
+
41
+ /** Build/source identity attached to observations so a new install is never mistaken for the running process. */
42
+ export interface BuildIdentity {
43
+ buildId?: string
44
+ revision?: string
45
+ label?: string
46
+ }
47
+
48
+ export interface SessionBinding {
49
+ sessionId: string
50
+ appId: string
51
+ platform: Platform
52
+ targetId: string
53
+ ownership: SessionOwnership
54
+ state: SessionState
55
+ createdAt: string
56
+ /** `fresh` = new process; `warm` = iteration that retained process/module/OS state. */
57
+ processKind: 'fresh' | 'warm' | null
58
+ buildIdentity?: BuildIdentity
59
+ detail?: string
60
+ }
61
+
62
+ export interface EndSessionResult {
63
+ sessionId: string
64
+ ended: boolean
65
+ /** Owned resources released (processes, ports, temp dirs) — reported by the adapter. */
66
+ released?: string[]
67
+ /** What was deliberately left running — e.g. an attached user-owned session. */
68
+ preserved?: string[]
69
+ }
70
+
71
+ export type JobState = 'queued' | 'running' | 'succeeded' | 'failed' | 'cancelling' | 'cancelled'
72
+
73
+ export interface JobProgress {
74
+ phase?: string
75
+ percent?: number
76
+ message?: string
77
+ }
78
+
79
+ export interface Job {
80
+ jobId: string
81
+ /** e.g. `build`, `launch`, `attach`, `dev-action`. */
82
+ kind: string
83
+ sessionId?: string
84
+ /** Idempotence key supplied by the caller or adapter. */
85
+ requestKey?: string
86
+ state: JobState
87
+ progress?: JobProgress
88
+ result?: unknown
89
+ error?: ContractError
90
+ createdAt: string
91
+ finishedAt?: string
92
+ }
93
+
94
+ /** Adapter-provided work for a job. Receives the cooperative-cancel signal. */
95
+ export type JobRunner = (
96
+ signal: AbortSignal,
97
+ report: (progress: JobProgress) => void,
98
+ ) => Promise<unknown>
99
+
100
+ export interface StartJobOptions {
101
+ kind: string
102
+ sessionId?: string
103
+ requestKey?: string
104
+ run?: JobRunner
105
+ }
106
+
107
+ export interface StartJobResult {
108
+ job: Job
109
+ /** True when an identical live job already existed and was returned instead of starting a duplicate. */
110
+ deduplicated: boolean
111
+ }
112
+
113
+ export interface SessionRegistry {
114
+ /** Start a server-owned session in `starting` state. `cleanup` runs on end/shutdown and must only release resources this session created. */
115
+ bindOwned(input: {
116
+ appId: string
117
+ platform: Platform
118
+ targetId: string
119
+ cleanup?: () => Promise<{ released?: string[] } | void> | { released?: string[] } | void
120
+ detail?: string
121
+ }): SessionBinding
122
+ /** Bind an existing user-owned development session — never restarted or terminated by this server. */
123
+ attach(input: {
124
+ appId: string
125
+ platform: Platform
126
+ targetId: string
127
+ processKind?: 'fresh' | 'warm'
128
+ buildIdentity?: BuildIdentity
129
+ detail?: string
130
+ }): SessionBinding
131
+ get(sessionId: string): SessionBinding | null
132
+ /** The binding if usable, else a ContractError: `stale_identity` (stopped) or `disconnected_runtime`. */
133
+ requireUsable(
134
+ sessionId: string,
135
+ ): { usable: true; session: SessionBinding } | { usable: false; error: ContractError }
136
+ markReady(
137
+ sessionId: string,
138
+ meta?: { processKind?: 'fresh' | 'warm'; buildIdentity?: BuildIdentity },
139
+ ): SessionBinding | null
140
+ markDisconnected(sessionId: string, detail?: string): SessionBinding | null
141
+ end(sessionId: string): Promise<EndSessionResult>
142
+ /** End every owned session and detach attached ones. */
143
+ shutdown(): Promise<EndSessionResult[]>
144
+ list(): SessionBinding[]
145
+ }
146
+
147
+ export interface JobRegistry {
148
+ start(options: StartJobOptions): StartJobResult
149
+ status(jobId: string): Job | null
150
+ setProgress(jobId: string, progress: JobProgress): Job | null
151
+ succeed(jobId: string, result?: unknown): Job | null
152
+ fail(jobId: string, error: ContractError): Job | null
153
+ /** Cooperative cancellation; returns the job's resulting state. */
154
+ cancel(jobId: string): Job | null
155
+ /** Live and finished jobs (finished history is bounded). */
156
+ list(): Job[]
157
+ }
158
+
159
+ export declare function createSessionRegistry(): SessionRegistry
160
+ export declare function createJobRegistry(options?: { historyLimit?: number }): JobRegistry
@@ -0,0 +1,3 @@
1
+ // Runtime contract moved to @octane-xplat/cli (shared agent service layer).
2
+ // This shim preserves the @octane-xplat/mcp ./contracts export surface.
3
+ export * from '@octane-xplat/cli/mcp/contracts/sessions'
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The project-status report — the single externally observable shape returned
3
+ * by `xplat_project_status` at setup time. Later packages fill their own
4
+ * slots through registered providers; absent providers read as
5
+ * `not_provided`, never as healthy or silently absent.
6
+ */
7
+
8
+ import type {
9
+ CodeIndexStatus,
10
+ DocsStatus,
11
+ GuidanceStatus,
12
+ ProjectIdentity,
13
+ RuntimePrerequisite,
14
+ } from './identity'
15
+
16
+ import type { PlatformCapabilityMatrix } from './capabilities'
17
+ import type { PermissionGrant } from './policy'
18
+
19
+ export interface ServerInfo {
20
+ name: string
21
+ version: string
22
+ }
23
+
24
+ export interface ProjectStatusReport {
25
+ server: ServerInfo
26
+ identity: ProjectIdentity
27
+ guidance: GuidanceStatus
28
+ docs: DocsStatus
29
+ codeIndex: CodeIndexStatus
30
+ /** Prerequisites relevant to the detected apps' declared platforms. */
31
+ prerequisites: RuntimePrerequisite[]
32
+ /** Matrix for the workspace's declared platforms. */
33
+ capabilities: PlatformCapabilityMatrix[]
34
+ /** Current permission grants and their states. */
35
+ permissions: PermissionGrant[]
36
+ /** What this product deliberately does not do. */
37
+ boundaries: readonly string[]
38
+ /** Ordered next steps for the agent, tailored to the report's actual state. */
39
+ nextSteps: string[]
40
+ }
@@ -0,0 +1,3 @@
1
+ // Runtime contract moved to @octane-xplat/cli (shared agent service layer).
2
+ // This shim preserves the @octane-xplat/mcp ./contracts export surface.
3
+ export * from '@octane-xplat/cli/mcp/contracts/status'
@@ -0,0 +1,2 @@
1
+ // Moved to @octane-xplat/cli (shared agent service layer); re-exported for compatibility.
2
+ export * from '@octane-xplat/cli/mcp/discovery'
package/src/docs.mjs ADDED
@@ -0,0 +1,84 @@
1
+ // Docs tool registration (MCP adapter). The provider itself lives in the
2
+ // shared service layer at @octane-xplat/cli/mcp/docs-provider — the CLI's
3
+ // `docs`/`guidance` commands project the same service.
4
+ import { z } from 'zod'
5
+ import { contractError, toContractError } from './contracts/errors.mjs'
6
+ import { PLATFORMS } from './contracts/identity.mjs'
7
+ import { resolveSelection } from './status.mjs'
8
+
9
+ export function registerDocsTools(server, context, respond) {
10
+ const run = (method) => async (args) => {
11
+ const permission = context.policy.require('workspace')
12
+ if (!permission.granted) {
13
+ return respond({ ok: false, error: permission.error })
14
+ }
15
+
16
+ const selection = resolveSelection(context)
17
+ if (selection.state !== 'bound') {
18
+ return respond({
19
+ ok: false,
20
+ error: contractError(
21
+ selection.state === 'ambiguous' ? 'ambiguous_selection' : 'invalid_selection',
22
+ 'Bind an app with xplat_select_app before retrieving docs.',
23
+ ),
24
+ })
25
+ }
26
+
27
+ try {
28
+ const data = await context.docs.serviceFor(selection.app)[method](args)
29
+ return respond({ ok: true, data })
30
+ } catch (error) {
31
+ return respond({ ok: false, error: toContractError(error) })
32
+ }
33
+ }
34
+
35
+ server.registerTool(
36
+ 'xplat_docs_search',
37
+ {
38
+ description:
39
+ 'Search the selected app’s installed-version docs. Results include version/snapshot provenance, unavailable packages, conflicts, and separately labeled project guidance.',
40
+ inputSchema: {
41
+ query: z.string().trim().min(1).max(500),
42
+ package: z.string().optional(),
43
+ version: z
44
+ .string()
45
+ .optional()
46
+ .describe('Explicit comparison version, visibly outside the installed binding.'),
47
+ platform: z.enum(PLATFORMS).optional(),
48
+ corpus: z
49
+ .enum(['guide', 'recipe', 'design-note', 'implementation-note'])
50
+ .optional()
51
+ .default('guide'),
52
+ limit: z.number().int().min(1).max(50).optional(),
53
+ },
54
+ },
55
+ run('search'),
56
+ )
57
+
58
+ server.registerTool(
59
+ 'xplat_docs_read',
60
+ {
61
+ description:
62
+ 'Read a bounded documentation page from a search ref, or package and path, preserving its version binding.',
63
+ inputSchema: {
64
+ ref: z.string().optional(),
65
+ package: z.string().optional(),
66
+ path: z.string().optional(),
67
+ version: z.string().optional(),
68
+ offset: z.number().int().min(0).optional(),
69
+ limit: z.number().int().min(1).max(20000).optional(),
70
+ },
71
+ },
72
+ run('read'),
73
+ )
74
+
75
+ server.registerTool(
76
+ 'xplat_guidance',
77
+ {
78
+ description:
79
+ 'Retrieve tailored guidance for the selected app’s actual installed package versions.',
80
+ inputSchema: {},
81
+ },
82
+ run('guidance'),
83
+ )
84
+ }
@@ -0,0 +1,5 @@
1
+ // The inspection service moved to @octane-xplat/cli (shared agent service
2
+ // layer); the MCP tool registration lives in ./tools.mjs. Both names are
3
+ // re-exported here so existing importers keep working.
4
+ export { createInspectionService } from '@octane-xplat/cli/mcp/inspection/service'
5
+ export { registerInspectionTools } from './tools.mjs'
@@ -0,0 +1,53 @@
1
+ // Inspection tool registration (MCP adapter). The service lives in the
2
+ // shared layer at @octane-xplat/cli/mcp/inspection/service; this file only
3
+ // binds it to MCP tools.
4
+ import { z } from 'zod'
5
+
6
+ export function registerInspectionTools(server, context, respond) {
7
+ const control = z.object({ kind: z.enum(['id', 'testID']), name: z.string().min(1).max(200) })
8
+ const run = (method) => async (args) => {
9
+ const data = await context.inspection[method](args)
10
+ return data.error ? respond({ ok: false, error: data.error }) : respond({ ok: true, data })
11
+ }
12
+
13
+ server.registerTool(
14
+ 'xplat_inspect',
15
+ {
16
+ description:
17
+ 'Bounded main-document named DOM hosts in the owned Chromium session. Explicit id/testID selector; duplicates fail. Password/private values are redacted. Includes document/session/build identity and coverage.',
18
+ inputSchema: {
19
+ sessionId: z.string(),
20
+ control: control.optional(),
21
+ offset: z.number().int().min(0).optional(),
22
+ limit: z.number().int().min(1).max(100).optional(),
23
+ },
24
+ },
25
+ run('inspect'),
26
+ )
27
+
28
+ server.registerTool(
29
+ 'xplat_interact',
30
+ {
31
+ description:
32
+ 'Send scoped input to one stable control from an inspected document. dom-event sends a CDP mouse click; os-input sends Space/Enter via the macOS event system to owned headed Chromium. Reserve desktop input for OS actions. Input delivery is not an app assertion.',
33
+ inputSchema: {
34
+ sessionId: z.string(),
35
+ documentId: z.string(),
36
+ control,
37
+ mechanism: z.enum(['dom-event', 'os-input']),
38
+ key: z.enum(['Space', 'Enter']).optional().default('Space'),
39
+ },
40
+ },
41
+ run('interact'),
42
+ )
43
+
44
+ server.registerTool(
45
+ 'xplat_screenshot',
46
+ {
47
+ description:
48
+ 'Capture the owned Chromium viewport only with a current visual-capture grant. Returns a session-scoped artifact and provenance; capability discovery never grants approval.',
49
+ inputSchema: { sessionId: z.string() },
50
+ },
51
+ run('screenshot'),
52
+ )
53
+ }
@@ -0,0 +1,2 @@
1
+ // Moved to @octane-xplat/cli (shared agent service layer); re-exported for compatibility.
2
+ export * from '@octane-xplat/cli/mcp/runtime/logs'
@@ -0,0 +1,2 @@
1
+ // Moved to @octane-xplat/cli (shared agent service layer); re-exported for compatibility.
2
+ export * from '@octane-xplat/cli/mcp/runtime/process'
@@ -0,0 +1,2 @@
1
+ // Moved to @octane-xplat/cli (shared agent service layer); re-exported for compatibility.
2
+ export * from '@octane-xplat/cli/mcp/runtime/service'
@@ -0,0 +1,2 @@
1
+ // Moved to @octane-xplat/cli (shared agent service layer); re-exported for compatibility.
2
+ export * from '@octane-xplat/cli/mcp/runtime/targets'
@@ -0,0 +1,126 @@
1
+ // Runtime MCP tools (work item D). Thin bindings over the runtime service —
2
+ // all session/job/ownership behavior lives in service.mjs and the shared
3
+ // contracts.
4
+
5
+ import { z } from 'zod'
6
+ import { PLATFORMS } from '../contracts/identity.mjs'
7
+
8
+ export function registerRuntimeTools(server, context, respond) {
9
+ const runtime = context.runtime
10
+
11
+ server.registerTool(
12
+ 'xplat_targets',
13
+ {
14
+ description:
15
+ 'Discover launchable targets for the bound app: simulators, emulators, devices and host targets with availability, missing-toolchain reasons, and session busy/ownership state.',
16
+ inputSchema: {
17
+ platform: z.enum(PLATFORMS).optional().describe('Limit to one platform'),
18
+ },
19
+ },
20
+ async ({ platform }) => {
21
+ const result = await runtime.listTargets({ platform })
22
+ return result.error
23
+ ? respond({ ok: false, error: result.error })
24
+ : respond({ ok: true, data: result })
25
+ },
26
+ )
27
+
28
+ server.registerTool(
29
+ 'xplat_build',
30
+ {
31
+ description:
32
+ 'Build the bound app for one platform as a background job (the same commands `xplat build` runs). Returns a job id — poll xplat_job_status and read output with xplat_logs. Repeating with the same requestKey returns the live job instead of duplicating it.',
33
+ inputSchema: {
34
+ platform: z.enum(PLATFORMS),
35
+ requestKey: z.string().optional().describe('Idempotence key for retries'),
36
+ },
37
+ },
38
+ async ({ platform, requestKey }) => {
39
+ const result = await runtime.build({ platform, requestKey })
40
+ return result.error
41
+ ? respond({ ok: false, error: result.error })
42
+ : respond({ ok: true, data: result })
43
+ },
44
+ )
45
+
46
+ server.registerTool(
47
+ 'xplat_launch',
48
+ {
49
+ description:
50
+ 'Start an owned development session: bind one app to one target and run the platform dev command as a job. The job completes when readiness is confirmed — a failed launch after a successful build reports accurately, and the session keeps the app alive until xplat_session_end.',
51
+ inputSchema: {
52
+ platform: z.enum(PLATFORMS),
53
+ targetId: z
54
+ .string()
55
+ .optional()
56
+ .describe('Target from xplat_targets; required when several are available'),
57
+ requestKey: z.string().optional().describe('Idempotence key for retries'),
58
+ browserMode: z
59
+ .enum(['headless', 'headed'])
60
+ .optional()
61
+ .describe(
62
+ 'Web only. Headed mode enables scoped OS keyboard input on qualified hosts; reserve desktop input before using it.',
63
+ ),
64
+ },
65
+ },
66
+ async ({ platform, targetId, requestKey, browserMode }) => {
67
+ const result = await runtime.launch({ platform, targetId, requestKey, browserMode })
68
+ return result.error
69
+ ? respond({ ok: false, error: result.error })
70
+ : respond({ ok: true, data: result })
71
+ },
72
+ )
73
+
74
+ server.registerTool(
75
+ 'xplat_attach',
76
+ {
77
+ description:
78
+ 'Attach to a compatible development session that is already running — never restarts or terminates it. Reports candidates per platform; attach binds a preserved user-owned session.',
79
+ inputSchema: {
80
+ platform: z.enum(PLATFORMS),
81
+ targetId: z
82
+ .string()
83
+ .optional()
84
+ .describe('Candidate target; required when several are attachable'),
85
+ },
86
+ },
87
+ async ({ platform, targetId }) => {
88
+ const result = await runtime.attach({ platform, targetId })
89
+ return result.error
90
+ ? respond({ ok: false, error: result.error })
91
+ : respond({ ok: true, data: result })
92
+ },
93
+ )
94
+
95
+ server.registerTool(
96
+ 'xplat_logs',
97
+ {
98
+ description:
99
+ 'Bounded, ordered log read for a session or build job. Cursor-paginated: pass the previous nextCursor to continue; truncated/dropped flags reveal gaps and reset marks a restarted capture. A stopped or disconnected session reports its capture as historical.',
100
+ inputSchema: {
101
+ sessionId: z.string().optional(),
102
+ jobId: z.string().optional(),
103
+ cursor: z.number().optional().describe('Continue after this record seq'),
104
+ limit: z.number().int().positive().max(5000).optional(),
105
+ stream: z.enum(['stdout', 'stderr', 'lifecycle']).optional(),
106
+ },
107
+ },
108
+ async ({ sessionId, jobId, cursor, limit, stream }) => {
109
+ if (!sessionId && !jobId) {
110
+ return respond({
111
+ ok: false,
112
+ error: {
113
+ code: 'invalid_selection',
114
+ message: 'Pass a sessionId or a jobId.',
115
+ recovery: { action: 'Use xplat_sessions or a build/launch job id.' },
116
+ },
117
+ })
118
+ }
119
+
120
+ const result = runtime.readLogs({ sessionId, jobId, cursor, limit, stream })
121
+ return result.error
122
+ ? respond({ ok: false, error: result.error })
123
+ : respond({ ok: true, data: result })
124
+ },
125
+ )
126
+ }