@octane-xplat/mcp 0.1.0 → 0.16.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 (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +342 -1
  3. package/dist/_virtual/_rolldown/runtime.js +27 -0
  4. package/dist/actions/bridge.js +1 -0
  5. package/dist/context.js +0 -0
  6. package/dist/contracts/actions.js +1 -0
  7. package/dist/contracts/boundaries.js +1 -0
  8. package/dist/contracts/capabilities.js +1 -0
  9. package/dist/contracts/code.js +1 -0
  10. package/dist/contracts/errors.js +8 -0
  11. package/dist/contracts/evidence.js +8 -0
  12. package/dist/contracts/identity.js +8 -0
  13. package/dist/contracts/index.js +1 -0
  14. package/dist/contracts/inspection.js +1 -0
  15. package/dist/contracts/policy.js +8 -0
  16. package/dist/contracts/sessions.js +1 -0
  17. package/dist/contracts/status.js +1 -0
  18. package/dist/discovery.js +8 -0
  19. package/dist/docs.js +63 -0
  20. package/dist/inspection/service.js +3 -0
  21. package/dist/inspection/tools.js +43 -0
  22. package/dist/runtime/logs.js +1 -0
  23. package/dist/runtime/process.js +1 -0
  24. package/dist/runtime/service.js +1 -0
  25. package/dist/runtime/targets.js +1 -0
  26. package/dist/runtime/tools.js +119 -0
  27. package/dist/server.js +324 -0
  28. package/dist/status.js +8 -0
  29. package/dist/xplat-mcp.js +68 -0
  30. package/package.json +43 -4
  31. package/src/actions/bridge.ts +2 -0
  32. package/src/context.ts +108 -0
  33. package/src/contracts/actions.ts +3 -0
  34. package/src/contracts/boundaries.ts +3 -0
  35. package/src/contracts/capabilities.ts +3 -0
  36. package/src/contracts/code.ts +3 -0
  37. package/src/contracts/errors.ts +3 -0
  38. package/src/contracts/evidence.ts +3 -0
  39. package/src/contracts/identity.ts +3 -0
  40. package/src/contracts/index.ts +3 -0
  41. package/src/contracts/inspection.ts +3 -0
  42. package/src/contracts/policy.ts +3 -0
  43. package/src/contracts/sessions.ts +3 -0
  44. package/src/contracts/status.ts +3 -0
  45. package/src/discovery.ts +2 -0
  46. package/src/docs.ts +89 -0
  47. package/src/inspection/service.ts +5 -0
  48. package/src/inspection/tools.ts +61 -0
  49. package/src/runtime/logs.ts +2 -0
  50. package/src/runtime/process.ts +2 -0
  51. package/src/runtime/service.ts +2 -0
  52. package/src/runtime/targets.ts +2 -0
  53. package/src/runtime/tools.ts +136 -0
  54. package/src/server.ts +580 -0
  55. package/src/status.ts +2 -0
  56. package/src/xplat-mcp.ts +96 -0
  57. package/types/contracts/actions.d.ts +209 -0
  58. package/types/contracts/boundaries.d.ts +6 -0
  59. package/types/contracts/capabilities.d.ts +106 -0
  60. package/types/contracts/code.d.ts +134 -0
  61. package/types/contracts/errors.d.ts +75 -0
  62. package/types/contracts/evidence.d.ts +115 -0
  63. package/types/contracts/identity.d.ts +127 -0
  64. package/types/contracts/index.d.ts +11 -0
  65. package/types/contracts/inspection.d.ts +45 -0
  66. package/types/contracts/policy.d.ts +86 -0
  67. package/types/contracts/sessions.d.ts +160 -0
  68. package/types/contracts/status.d.ts +40 -0
package/src/context.ts ADDED
@@ -0,0 +1,108 @@
1
+ // Shared internal types for the MCP transport adapter. The context object is
2
+ // assembled by createWorkspaceContext in the shared service layer
3
+ // (@octane-xplat/cli/mcp/status); its registries/stores are typed by the
4
+ // published contracts while service slots (runtime, docs, inspection,
5
+ // dev-actions) keep the adapter-facing surface the tools actually use.
6
+
7
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
8
+ import type { discoverWorkspace } from '@octane-xplat/cli/mcp/discovery'
9
+ import type { CapabilityRegistry } from '../types/contracts/capabilities'
10
+ import type { CodeProvider } from '../types/contracts/code'
11
+ import type { ToolOutcome } from '../types/contracts/errors'
12
+ import type { EvidenceStore } from '../types/contracts/evidence'
13
+ import type { AppRef, Platform } from '../types/contracts/identity'
14
+ import type { PolicyStore } from '../types/contracts/policy'
15
+ import type { Job, JobRegistry, SessionRegistry } from '../types/contracts/sessions'
16
+ import type { ServerInfo } from '../types/contracts/status'
17
+
18
+ export type Discovery = ReturnType<typeof discoverWorkspace>
19
+
20
+ /** The app-binding result: explicit/configured binding, sole app, or a list the developer must choose from. */
21
+ export type Selection =
22
+ | { state: 'bound'; app: AppRef; reason: string | null }
23
+ | { state: 'ambiguous'; candidates: AppRef[] }
24
+ | { state: 'none'; reason: string }
25
+
26
+ export type SelectionReason = 'explicit' | 'configured' | 'sole-app' | null
27
+
28
+ export interface RuntimeService {
29
+ registerCapabilities(): void
30
+ listTargets(args: { platform?: Platform }): Promise<any>
31
+ build(args: { platform: Platform; requestKey?: string }): Promise<any>
32
+ launch(args: {
33
+ platform: Platform
34
+ targetId?: string
35
+ requestKey?: string
36
+ browserMode?: 'headless' | 'headed'
37
+ }): Promise<any>
38
+ attach(args: { platform: Platform; targetId?: string }): Promise<any>
39
+ readLogs(args: {
40
+ sessionId?: string
41
+ jobId?: string
42
+ cursor?: number
43
+ limit?: number
44
+ stream?: 'stdout' | 'stderr' | 'lifecycle'
45
+ }): any
46
+ endSession(sessionId: string): Promise<any>
47
+ endAll(): Promise<any>
48
+ }
49
+
50
+ export interface DocsProvider {
51
+ status(app: AppRef | null): unknown
52
+ serviceFor(app: AppRef): {
53
+ search(args: any): Promise<any>
54
+ read(args: any): Promise<any>
55
+ guidance(args: any): Promise<any>
56
+ }
57
+ }
58
+
59
+ export interface InspectionService {
60
+ inspect(args: any): Promise<any>
61
+ interact(args: any): Promise<any>
62
+ screenshot(args: any): Promise<any>
63
+ }
64
+
65
+ export interface DevActionsService {
66
+ list(app: AppRef): Promise<any>
67
+ invoke(
68
+ app: AppRef,
69
+ args: {
70
+ appInstanceId?: string
71
+ actionId: string
72
+ input: Record<string, unknown>
73
+ registryRevision?: number
74
+ requestKey?: string
75
+ },
76
+ ): Promise<any>
77
+ describeJob(jobId: string): { job: Job; [key: string]: unknown } | null
78
+ }
79
+
80
+ export interface WorkspaceContext {
81
+ workspaceRoot: string
82
+ configuredAppSelector: string | null
83
+ serverIdentity: ServerInfo
84
+ discovery: Discovery
85
+ boundApp: AppRef | null
86
+ selectionReason: SelectionReason
87
+ capabilities: CapabilityRegistry
88
+ policy: PolicyStore
89
+ sessions: SessionRegistry
90
+ jobs: JobRegistry
91
+ evidence: EvidenceStore
92
+ docsStatus: (app: AppRef | null) => unknown
93
+ codeIndexStatus: (app: AppRef | null) => unknown
94
+ code: CodeProvider | null
95
+ docs: DocsProvider
96
+ devActions: DevActionsService
97
+ runtime: RuntimeService
98
+ inspection: InspectionService
99
+ }
100
+
101
+ /** The structuredContent envelope every tool result shares. */
102
+ export interface ToolResponse extends CallToolResult {
103
+ structuredContent: ToolOutcome<unknown>
104
+ isError: boolean
105
+ content: [{ type: 'text'; text: string }]
106
+ }
107
+
108
+ export type Respond = (outcome: ToolOutcome<unknown>) => ToolResponse
@@ -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/actions'
@@ -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/boundaries'
@@ -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/capabilities'
@@ -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/code'
@@ -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/errors'
@@ -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/evidence'
@@ -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/identity'
@@ -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/index'
@@ -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/inspection'
@@ -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,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,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.ts ADDED
@@ -0,0 +1,89 @@
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 type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
6
+ import { contractError, toContractError } from './contracts/errors.ts'
7
+ import { PLATFORMS } from './contracts/identity.ts'
8
+ import { resolveSelection } from './status.ts'
9
+ import type { Respond, Selection, WorkspaceContext } from './context.ts'
10
+ import type { Platform } from '../types/contracts/identity'
11
+
12
+ export function registerDocsTools(server: McpServer, context: WorkspaceContext, respond: Respond) {
13
+ const run =
14
+ (method: 'search' | 'read' | 'guidance') =>
15
+ async (args: any): Promise<ReturnType<Respond>> => {
16
+ const permission = context.policy.require('workspace')
17
+ if (!permission.granted) {
18
+ return respond({ ok: false, error: permission.error })
19
+ }
20
+
21
+ const selection = resolveSelection(context) as Selection
22
+ if (selection.state !== 'bound') {
23
+ return respond({
24
+ ok: false,
25
+ error: contractError(
26
+ selection.state === 'ambiguous' ? 'ambiguous_selection' : 'invalid_selection',
27
+ 'Bind an app with xplat_select_app before retrieving docs.',
28
+ ),
29
+ })
30
+ }
31
+
32
+ try {
33
+ const data = await context.docs.serviceFor(selection.app)[method](args)
34
+ return respond({ ok: true, data })
35
+ } catch (error) {
36
+ return respond({ ok: false, error: toContractError(error) })
37
+ }
38
+ }
39
+
40
+ server.registerTool(
41
+ 'xplat_docs_search',
42
+ {
43
+ description:
44
+ 'Search the selected app’s installed-version docs. Results include version/snapshot provenance, unavailable packages, conflicts, and separately labeled project guidance.',
45
+ inputSchema: {
46
+ query: z.string().trim().min(1).max(500),
47
+ package: z.string().optional(),
48
+ version: z
49
+ .string()
50
+ .optional()
51
+ .describe('Explicit comparison version, visibly outside the installed binding.'),
52
+ platform: z.enum(PLATFORMS as Platform[]).optional(),
53
+ corpus: z
54
+ .enum(['guide', 'recipe', 'design-note', 'implementation-note'])
55
+ .optional()
56
+ .default('guide'),
57
+ limit: z.number().int().min(1).max(50).optional(),
58
+ },
59
+ },
60
+ run('search'),
61
+ )
62
+
63
+ server.registerTool(
64
+ 'xplat_docs_read',
65
+ {
66
+ description:
67
+ 'Read a bounded documentation page from a search ref, or package and path, preserving its version binding.',
68
+ inputSchema: {
69
+ ref: z.string().optional(),
70
+ package: z.string().optional(),
71
+ path: z.string().optional(),
72
+ version: z.string().optional(),
73
+ offset: z.number().int().min(0).optional(),
74
+ limit: z.number().int().min(1).max(20000).optional(),
75
+ },
76
+ },
77
+ run('read'),
78
+ )
79
+
80
+ server.registerTool(
81
+ 'xplat_guidance',
82
+ {
83
+ description:
84
+ 'Retrieve tailored guidance for the selected app’s actual installed package versions.',
85
+ inputSchema: {},
86
+ },
87
+ run('guidance'),
88
+ )
89
+ }
@@ -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.ts'
@@ -0,0 +1,61 @@
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
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
6
+ import type { Respond, WorkspaceContext } from '../context.ts'
7
+
8
+ export function registerInspectionTools(
9
+ server: McpServer,
10
+ context: WorkspaceContext,
11
+ respond: Respond,
12
+ ) {
13
+ const control = z.object({ kind: z.enum(['id', 'testID']), name: z.string().min(1).max(200) })
14
+ const run =
15
+ (method: 'inspect' | 'interact' | 'screenshot') =>
16
+ async (args: any): Promise<ReturnType<Respond>> => {
17
+ const data = await context.inspection[method](args)
18
+ return data.error ? respond({ ok: false, error: data.error }) : respond({ ok: true, data })
19
+ }
20
+
21
+ server.registerTool(
22
+ 'xplat_inspect',
23
+ {
24
+ description:
25
+ '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.',
26
+ inputSchema: {
27
+ sessionId: z.string(),
28
+ control: control.optional(),
29
+ offset: z.number().int().min(0).optional(),
30
+ limit: z.number().int().min(1).max(100).optional(),
31
+ },
32
+ },
33
+ run('inspect'),
34
+ )
35
+
36
+ server.registerTool(
37
+ 'xplat_interact',
38
+ {
39
+ description:
40
+ '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.',
41
+ inputSchema: {
42
+ sessionId: z.string(),
43
+ documentId: z.string(),
44
+ control,
45
+ mechanism: z.enum(['dom-event', 'os-input']),
46
+ key: z.enum(['Space', 'Enter']).optional().default('Space'),
47
+ },
48
+ },
49
+ run('interact'),
50
+ )
51
+
52
+ server.registerTool(
53
+ 'xplat_screenshot',
54
+ {
55
+ description:
56
+ 'Capture the owned Chromium viewport only with a current visual-capture grant. Returns a session-scoped artifact and provenance; capability discovery never grants approval.',
57
+ inputSchema: { sessionId: z.string() },
58
+ },
59
+ run('screenshot'),
60
+ )
61
+ }
@@ -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,136 @@
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 type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
7
+ import { PLATFORMS } from '../contracts/identity.ts'
8
+ import type { Respond, WorkspaceContext } from '../context.ts'
9
+ import type { Platform } from '../../types/contracts/identity'
10
+
11
+ export function registerRuntimeTools(
12
+ server: McpServer,
13
+ context: WorkspaceContext,
14
+ respond: Respond,
15
+ ) {
16
+ const runtime = context.runtime
17
+
18
+ server.registerTool(
19
+ 'xplat_targets',
20
+ {
21
+ description:
22
+ 'Discover launchable targets for the bound app: simulators, emulators, devices and host targets with availability, missing-toolchain reasons, and session busy/ownership state.',
23
+ inputSchema: {
24
+ platform: z
25
+ .enum(PLATFORMS as Platform[])
26
+ .optional()
27
+ .describe('Limit to one platform'),
28
+ },
29
+ },
30
+ async ({ platform }) => {
31
+ const result = await runtime.listTargets({ platform })
32
+ return result.error
33
+ ? respond({ ok: false, error: result.error })
34
+ : respond({ ok: true, data: result })
35
+ },
36
+ )
37
+
38
+ server.registerTool(
39
+ 'xplat_build',
40
+ {
41
+ description:
42
+ '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.',
43
+ inputSchema: {
44
+ platform: z.enum(PLATFORMS as Platform[]),
45
+ requestKey: z.string().optional().describe('Idempotence key for retries'),
46
+ },
47
+ },
48
+ async ({ platform, requestKey }) => {
49
+ const result = await runtime.build({ platform, requestKey })
50
+ return result.error
51
+ ? respond({ ok: false, error: result.error })
52
+ : respond({ ok: true, data: result })
53
+ },
54
+ )
55
+
56
+ server.registerTool(
57
+ 'xplat_launch',
58
+ {
59
+ description:
60
+ '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.',
61
+ inputSchema: {
62
+ platform: z.enum(PLATFORMS as Platform[]),
63
+ targetId: z
64
+ .string()
65
+ .optional()
66
+ .describe('Target from xplat_targets; required when several are available'),
67
+ requestKey: z.string().optional().describe('Idempotence key for retries'),
68
+ browserMode: z
69
+ .enum(['headless', 'headed'])
70
+ .optional()
71
+ .describe(
72
+ 'Web only. Headed mode enables scoped OS keyboard input on qualified hosts; reserve desktop input before using it.',
73
+ ),
74
+ },
75
+ },
76
+ async ({ platform, targetId, requestKey, browserMode }) => {
77
+ const result = await runtime.launch({ platform, targetId, requestKey, browserMode })
78
+ return result.error
79
+ ? respond({ ok: false, error: result.error })
80
+ : respond({ ok: true, data: result })
81
+ },
82
+ )
83
+
84
+ server.registerTool(
85
+ 'xplat_attach',
86
+ {
87
+ description:
88
+ '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.',
89
+ inputSchema: {
90
+ platform: z.enum(PLATFORMS as Platform[]),
91
+ targetId: z
92
+ .string()
93
+ .optional()
94
+ .describe('Candidate target; required when several are attachable'),
95
+ },
96
+ },
97
+ async ({ platform, targetId }) => {
98
+ const result = await runtime.attach({ platform, targetId })
99
+ return result.error
100
+ ? respond({ ok: false, error: result.error })
101
+ : respond({ ok: true, data: result })
102
+ },
103
+ )
104
+
105
+ server.registerTool(
106
+ 'xplat_logs',
107
+ {
108
+ description:
109
+ '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.',
110
+ inputSchema: {
111
+ sessionId: z.string().optional(),
112
+ jobId: z.string().optional(),
113
+ cursor: z.number().optional().describe('Continue after this record seq'),
114
+ limit: z.number().int().positive().max(5000).optional(),
115
+ stream: z.enum(['stdout', 'stderr', 'lifecycle']).optional(),
116
+ },
117
+ },
118
+ async ({ sessionId, jobId, cursor, limit, stream }) => {
119
+ if (!sessionId && !jobId) {
120
+ return respond({
121
+ ok: false,
122
+ error: {
123
+ code: 'invalid_selection',
124
+ message: 'Pass a sessionId or a jobId.',
125
+ recovery: { action: 'Use xplat_sessions or a build/launch job id.' },
126
+ },
127
+ })
128
+ }
129
+
130
+ const result = runtime.readLogs({ sessionId, jobId, cursor, limit, stream })
131
+ return result.error
132
+ ? respond({ ok: false, error: result.error })
133
+ : respond({ ok: true, data: result })
134
+ },
135
+ )
136
+ }