@lincyaw/mcp 0.0.0-stage → 1.0.0-rc.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.
package/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
package/README.md CHANGED
@@ -1,3 +1,91 @@
1
- # Temporary Holding Version
1
+ # @lincyaw/mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Presmium's agent environment: a document's tools and resources over MCP, for agents such as Claude Code, Codex or VS Code.
4
+
5
+ Part of [Presmium](https://github.com/Lincyaw/presmium), which compiles canonical lecture.json teaching documents into web pages, slides, Jupyter notebooks and narrated playback.
6
+
7
+ ## Connect
8
+
9
+ There are two servers.
10
+
11
+ **Workspace server (HTTP).** `presmium dev` and `presmium serve` serve MCP at `/mcp` over Streamable HTTP and print its address and the file that holds its bearer token:
12
+
13
+ ```sh
14
+ presmium serve
15
+ # mcp <url>/mcp (its token is in <state file>)
16
+ ```
17
+
18
+ Point the client at that URL with `Authorization: Bearer <token>`. This server has everything below: authoring, history, projects, transcription, voiceover, audio and images, builds, the author's browser runtime and focus, and several documents (name one with `document`; see `list_documents`).
19
+
20
+ **Compiler-only server (stdio).** Without a running server, serve one document on stdio:
21
+
22
+ ```sh
23
+ pnpm exec presmium-mcp <document directory> [--allow operate,spend]
24
+ ```
25
+
26
+ It compiles the document itself and offers reading, diagnostics, skills, `run_code` and `build`. It has no workspace services: authoring, history, transcription, voiceover and audio tools report themselves unavailable. Operations are offered only for the kinds `--allow` names. For skills alone, pass an existing empty directory.
27
+
28
+ Plugin tools appear once their compilation is ready; follow tool-list change notifications.
29
+
30
+ ## Tools
31
+
32
+ | Group | Main tools |
33
+ | --- | --- |
34
+ | Skills | `list_skills`, `read_skill` |
35
+ | Reading | `list_documents`, `deck_info`, `list_sections`, `get_section`, `get_step`, `search`, `get_code_ref`, `get_outputs`, `locate_source` |
36
+ | Diagnostics | `get_diagnostics` (reads the snapshot), `check_document` (runs compilation and plugin checks) |
37
+ | Runtime and focus | `get_runtime`, `get_focus`, `get_position`, `navigate` |
38
+ | Run and build | `run_code`, `build`, `get_builds`, `cancel_build` |
39
+ | Classroom | `get_classroom`, `classroom_start`, `classroom_end` |
40
+ | Canonical reading | `get_authoring_capabilities`, `get_document_outline`, `read_document_part`, `get_document` (full document, may exceed a tool-result budget) |
41
+ | Document changes | `plan_document_change`, `apply_document_change` |
42
+ | Pages | `plan_page_change`, `apply_page_change`, `plan_page_duplication`, `apply_page_duplication` |
43
+ | Comments, voice settings, component parameters, caption timing | `plan_authoring_action`, `apply_authoring_action`, `read_authoring_action_impact` |
44
+ | History | `get_document_history`, `read_history_file` |
45
+ | Projects | `get_project_capabilities`, `plan_project_change`, `apply_project_change` |
46
+ | Transcription | `get_transcription_capabilities`, `start_transcription`, `get_transcriptions`, `get_transcription`, `get_transcription_result`, `cancel_transcription`, `plan_transcription_adoption`, `apply_transcription_adoption` |
47
+ | Voice and voiceover | `get_voice_authoring_capabilities`, `get_voice_catalog`, `get_voiceover_status`, `start_voiceover`, `get_voiceover_job`, `get_voiceover_jobs`, `cancel_voiceover`, `quote_voice_preview`, `preview_voice` |
48
+ | Audio and clips | `get_audio_authoring_capabilities`, `inspect_workspace_audio`, `import_workspace_audio`, `inspect_document_audio`, `adopt_recording`, `read_production_plan`, `validate_media_clip_change`, `change_media_clip` |
49
+ | Images | `get_image_authoring_capabilities`, `inspect_workspace_image`, `import_workspace_image` |
50
+ | Browser clients | `get_authoring_clients`, `request_authoring_client`, `get_authoring_client_request` |
51
+
52
+ `get_section` and `get_step` read derived runtime data. To edit, read canonical content with `get_document_outline` and `read_document_part`, then plan and apply. Every change follows the same pattern: a `plan_*` call returns a bounded plan with source, plan id and digest; the matching `apply_*` call commits exactly that plan through the same service the Web editor uses, and reports the saved result. A saved result does not prove compilation or playback. Deletions also need `confirmDeletion: true`. A source change between plan and apply returns 409; plan again instead of retrying with a new id.
53
+
54
+ ## Resources
55
+
56
+ | URI | Content |
57
+ | --- | --- |
58
+ | `presmium://documents` | Documents served |
59
+ | `presmium://deck` | Deck summary |
60
+ | `presmium://sections`, `presmium://sections/{section}`, `presmium://sections/{section}/steps/{step}`, `presmium://sections/{section}/outputs` | Sections, steps and recorded outputs |
61
+ | `presmium://ir` | Compiled deck (reading it prepares the IR when needed) |
62
+ | `presmium://diagnostics` | Current diagnostic snapshot; subscribe to hear of changes |
63
+ | `presmium://runtime` | Reports from the author's open browser windows |
64
+ | `presmium://focus` | What the author points at in the workbench; subscribable |
65
+ | `presmium://position` | Presentation position; subscribable |
66
+ | `presmium://classroom` | Classroom state |
67
+ | `presmium://skills`, `presmium://skills/{name}/{path}` | Skill catalog and paged skill files |
68
+ | `presmium://authoring-action/…`, `presmium://page-change/…`, `presmium://page-duplication/…` | Full candidates and impact of a plan |
69
+ | `presmium://voice-preview/{id}` | Audio of a generated voice preview |
70
+
71
+ With several documents, add `?document=<dir>`, for example `presmium://sections?document=lectures/intro`.
72
+
73
+ ## Approval
74
+
75
+ Each tool has a kind: `read`, or one of the approval kinds `operate`, `spend`, `write` and `agent`. Read tools run without asking. A read-only token sees no operations. On the workspace server, every other tool waits for the author to allow it in the page, or for the client's host to confirm it by Presmium's approval policy; a refusal fails with 403 and retrying asks again. Spending actions (`start_transcription`, `start_voiceover`, `preview_voice`) need their own approval. A token bound to one document cannot act on another. On stdio, `--allow` decides which kinds are offered and allowed.
76
+
77
+ ## Not supported
78
+
79
+ - Compiler-only stdio has no workspace services (authoring, history, projects, transcription, voiceover, audio).
80
+ - Retry receipts and plans are kept per process only.
81
+ - Configurations that require agent review reject canonical apply.
82
+ - Large generic `apply_document_change` inputs and results are not bounded.
83
+
84
+ ## Documentation
85
+
86
+ - [Author actions](../../docs/authoring-actions.md): the shared plan, commit and verify protocol, limits, recording, transcription, voiceover, audio, history and diagnostics.
87
+ - [Author guide](https://github.com/Lincyaw/presmium/blob/main/site/guide/locales/en/lecture.json): agents in your editor.
88
+
89
+ ## License
90
+
91
+ Apache-2.0
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import { createJiti } from 'jiti'
3
+
4
+ console.log = console.info = console.debug = console.error
5
+
6
+ const emitWarning = process.emitWarning
7
+ process.emitWarning = (warning, ...args) => {
8
+ const code = typeof args[0] === 'object' ? args[0]?.code : args[1]
9
+ if (code !== 'DEP0040') emitWarning.call(process, warning, ...args)
10
+ }
11
+
12
+ const jiti = createJiti(import.meta.url)
13
+ const { main } = await jiti.import('../src/cli.ts')
14
+ const code = await main(process.argv.slice(2))
15
+ if (code !== undefined) process.exitCode = code
package/package.json CHANGED
@@ -1,6 +1,45 @@
1
1
  {
2
2
  "name": "@lincyaw/mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.0-rc.1",
4
+ "description": "Presmium's agent environment: a document's tools and resources over MCP, and presmium-mcp over stdio",
5
+ "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/Lincyaw/presmium.git",
9
+ "directory": "packages/mcp"
10
+ },
11
+ "homepage": "https://github.com/Lincyaw/presmium/tree/main/packages/mcp#readme",
12
+ "bugs": "https://github.com/Lincyaw/presmium/issues",
13
+ "keywords": [
14
+ "presmium",
15
+ "mcp",
16
+ "agent"
17
+ ],
18
+ "type": "module",
19
+ "engines": {
20
+ "node": ">=22.19"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "bin": {
26
+ "presmium-mcp": "./bin/presmium-mcp.mjs"
27
+ },
28
+ "exports": {
29
+ ".": "./src/index.ts",
30
+ "./client": "./src/client.ts"
31
+ },
32
+ "files": [
33
+ "bin",
34
+ "src"
35
+ ],
36
+ "dependencies": {
37
+ "@modelcontextprotocol/client": "2.3.1",
38
+ "@modelcontextprotocol/server": "2.3.1",
39
+ "jiti": "^2.7.0",
40
+ "minisearch": "7.2.0",
41
+ "@lincyaw/skills": "1.0.0-rc.1",
42
+ "@lincyaw/kernel": "1.0.0-rc.1",
43
+ "@lincyaw/compiler": "1.0.0-rc.1"
44
+ }
6
45
  }
@@ -0,0 +1,91 @@
1
+ import { DOCUMENT_RECORDING_ADOPTION_SCHEMA, DOCUMENT_AUDIO_INSPECTION_SCHEMA, DOCUMENT_AUDIO_WORKSPACE_IMPORT_SCHEMA, DOCUMENT_WORKSPACE_AUDIO_INSPECTION_SCHEMA, DOCUMENT_MEDIA_CLIP_VALIDATION_SCHEMA, DOCUMENT_MEDIA_CLIP_CHANGE_SCHEMA, DOCUMENT_PRODUCTION_PLAN_SCHEMA } from '@lincyaw/kernel'
2
+ import type { DocumentRecordingAdoption, DocumentAudioAuthoring, DocumentAudioInspect, DocumentWorkspaceAudioImport, DocumentWorkspaceAudioInspect, DocumentMediaClipValidation, DocumentMediaClipEdit, DocProductionPlanQuery } from '@lincyaw/kernel'
3
+ import type { Call, ToolSpec } from './mcp.ts'
4
+
5
+ const document = { type: 'string', description: 'The served document directory; file paths remain workspace-relative within this selected document.' }
6
+ const input = (properties: Record<string, unknown>, required: string[]) => ({ type: 'object', properties: { document, ...properties }, required, additionalProperties: false })
7
+ const service = (call: Call): DocumentAudioAuthoring => {
8
+ if (!call.host.audioAuthoring) throw Object.assign(new Error('audio-authoring-host-unavailable'), { status: 503 })
9
+ return call.host.audioAuthoring
10
+ }
11
+
12
+ export const AUDIO_AUTHORING_TOOLS: ToolSpec[] = [
13
+ {
14
+ name: 'adopt_recording', kind: 'operate', structured: true,
15
+ description: 'Explicitly adopts one whole local audio asset as the narration source for one stable step. Optional sourceRange edits only an already adopted recording of this same asset in this step, against real inspected duration. Requires exact saved source and original SHA-256; preserves all narration, pronunciation, cues, ASR provenance and media clips. Trimmed narration keeps whole-step display captions with alignment:none and requires caption review; it does not align words or reuse whole-file ASR timing. Uses the shared Web canonical CAS/history/receipt boundary and cannot bypass required review. Unsupported cue timing is refused; no compilation, recognition, synthesis or implicit playback occurs. Read the real production plan after explicitly preparing IR to verify playback.',
16
+ inputSchema: input({ request: structuredClone(DOCUMENT_RECORDING_ADOPTION_SCHEMA) }, ['request']),
17
+ run: async call => {
18
+ const selected = await call.document(), request = call.args.request as DocumentRecordingAdoption, authoring = call.host.authoring?.capabilities(selected, call.caller.member)
19
+ if (authoring && !authoring.actions.apply) throw Object.assign(new Error(authoring.applyUnavailableReason === 'review-required' ? 'authoring-review-required' : 'authoring-permission-denied'), { status: authoring.applyUnavailableReason ? 409 : 403 })
20
+ const target = (await call.host.documents()).find(document => document.dir === selected)
21
+ await call.approve('operate', messages => messages.adoptRecording(request.stepId), { lang: target?.lang ?? '', detail: target?.title ?? selected, check: () => service(call).checkRecordingAdoption(selected, request, call.caller.member, call.signal) })
22
+ call.signal.throwIfAborted()
23
+ return service(call).adoptRecording(selected, request, call.caller.member, call.signal)
24
+ },
25
+ },
26
+ {
27
+ name: 'get_audio_authoring_capabilities', kind: 'read', structured: true,
28
+ description: 'Reports real canonical audio authoring support, local file scope, size limits, review requirements and receipt lifetime. Does not compile scenes, read audio bytes or recognize speech. Compiler-only stdio reports this workspace service unavailable.',
29
+ inputSchema: input({}, []),
30
+ run: async call => {
31
+ if (!call.host.audioAuthoring) return { available: false, status: 'unavailable', reason: 'host-unavailable', actions: { inspect: false, inspectWorkspace: false, importWorkspace: false, readProductionPlan: false, validateClipChange: false, changeClip: false, adoptRecording: false } }
32
+ const capability = await service(call).capabilities(await call.document(), call.caller.member)
33
+ return { ...capability, actions: { ...capability.actions, importWorkspace: capability.actions.importWorkspace && call.host.offers('operate'), changeClip: capability.actions.changeClip && call.host.offers('operate'), adoptRecording: capability.actions.adoptRecording && call.host.offers('operate') },
34
+ ...capability.actions.importWorkspace && !call.host.offers('operate') ? { importUnavailableReason: 'operation-disabled' } : {},
35
+ ...capability.actions.changeClip && !call.host.offers('operate') ? { clipChangeUnavailableReason: 'operation-disabled' } : {} }
36
+ },
37
+ },
38
+ {
39
+ name: 'inspect_document_audio', kind: 'read', structured: true,
40
+ description: 'Inspects a declared audio asset from the saved canonical document, including unused assets. Verifies exact source and actual bytes, duration and format through the same media service Web uses. No scene compilation or speech call.',
41
+ inputSchema: input({ request: structuredClone(DOCUMENT_AUDIO_INSPECTION_SCHEMA) }, ['request']),
42
+ run: async call => service(call).inspect(await call.document(), call.args.request as DocumentAudioInspect, call.caller.member, call.signal),
43
+ },
44
+ {
45
+ name: 'inspect_workspace_audio', kind: 'read', structured: true,
46
+ description: 'Inspects one permitted regular audio file in the selected document workspace scope, without importing it. Returns actual source, original SHA-256, byte length and media metadata; pass its exact file object to import_workspace_audio. No canonical asset identity is assigned, no scene is compiled and no speech provider is called.',
47
+ inputSchema: input({ request: structuredClone(DOCUMENT_WORKSPACE_AUDIO_INSPECTION_SCHEMA) }, ['request']),
48
+ run: async call => service(call).inspectWorkspace(await call.document(), call.args.request as DocumentWorkspaceAudioInspect, call.caller.member, call.signal),
49
+ },
50
+ {
51
+ name: 'read_production_plan', kind: 'read', structured: true,
52
+ description: 'Reads the real cache-only production plan for an exact saved source and an existing IR snapshot, optionally requiring a known IR revision. Returns the actual binding, confirmed or unknown step ranges and diagnostics without compiling, rendering or synthesizing. Missing IR is unavailable; explicitly read presmium://ir?document=<directory> when the snapshot needs preparation or refresh, then call this tool with source. Use its returned binding for media-clip changes; estimated timing is not confirmed timing.',
53
+ inputSchema: input({ request: structuredClone(DOCUMENT_PRODUCTION_PLAN_SCHEMA) }, ['request']),
54
+ run: async call => service(call).readProductionPlan(await call.document(), call.args.request as DocProductionPlanQuery, call.caller.member, call.signal),
55
+ },
56
+ {
57
+ name: 'validate_media_clip_change', kind: 'read', structured: true,
58
+ description: 'Checks one clip add, update or removal against exact saved source without writing. Upserts recheck real asset bytes and the current production binding, source range and confirmed step duration; removal needs no production plan. Returns the concrete before/after clip and affected steps/assets. This check creates no retained plan and apply checks everything again.',
59
+ inputSchema: input({ request: structuredClone(DOCUMENT_MEDIA_CLIP_VALIDATION_SCHEMA) }, ['request']),
60
+ run: async call => service(call).validateClipChange(await call.document(), call.args.request as DocumentMediaClipValidation, call.caller.member, call.signal),
61
+ },
62
+ {
63
+ name: 'change_media_clip', kind: 'operate', structured: true,
64
+ description: 'Adds, updates or removes one media clip after host authorization, using the same real validation, canonical CAS, lock, history transaction and receipts as Web. Uses agent origin and cannot bypass required review. Upserts require a current real production binding; removal remains available to repair a bad plan. Returns saved-source readback; it does not claim the new production plan or playback is ready.',
65
+ inputSchema: input({ request: structuredClone(DOCUMENT_MEDIA_CLIP_CHANGE_SCHEMA) }, ['request']),
66
+ run: async call => {
67
+ const selected = await call.document(), request = call.args.request as DocumentMediaClipEdit, authoring = call.host.authoring?.capabilities(selected, call.caller.member)
68
+ if (authoring && !authoring.actions.apply) throw Object.assign(new Error(authoring.applyUnavailableReason === 'review-required' ? 'authoring-review-required' : 'authoring-permission-denied'), { status: authoring.applyUnavailableReason ? 409 : 403 })
69
+ const clipId = 'clip' in request ? request.clip.id : request.removeClipId
70
+ const current = await call.host.authoring?.read(selected, call.caller.member)
71
+ const action = 'removeClipId' in request ? 'remove' : current?.content.production.mediaClips.some(clip => clip.id === clipId) ? 'update' : 'add'
72
+ const target = (await call.host.documents()).find(document => document.dir === selected)
73
+ await call.approve('operate', messages => messages.changeMediaClip(action, clipId), { lang: target?.lang ?? '', detail: target?.title ?? selected, check: () => service(call).checkClipChange(selected, request, call.caller.member, call.signal) })
74
+ call.signal.throwIfAborted()
75
+ return service(call).changeClip(selected, request, call.caller.member, call.signal)
76
+ },
77
+ },
78
+ {
79
+ name: 'import_workspace_audio', kind: 'operate', structured: true,
80
+ description: 'Imports one existing regular workspace audio file allowed by the selected document scope, after host authorization. Requires exact canonical source, expected original SHA-256 and byte length. Shares Web media validation, binary/document CAS, rollback and receipts; history origin is agent and required review cannot be bypassed. Returns saved-source and actual-file verification, without clips, narration, recognition, synthesis or playback.',
81
+ inputSchema: input({ request: structuredClone(DOCUMENT_AUDIO_WORKSPACE_IMPORT_SCHEMA) }, ['request']),
82
+ run: async call => {
83
+ const selected = await call.document(), request = call.args.request as DocumentWorkspaceAudioImport, authoring = call.host.authoring?.capabilities(selected, call.caller.member)
84
+ if (authoring && !authoring.actions.apply) throw Object.assign(new Error(authoring.applyUnavailableReason === 'review-required' ? 'authoring-review-required' : 'authoring-permission-denied'), { status: authoring.applyUnavailableReason ? 409 : 403 })
85
+ const target = (await call.host.documents()).find(document => document.dir === selected)
86
+ await call.approve('operate', messages => messages.importAudio(request.file.path.split('/').at(-1) ?? request.file.path), { lang: target?.lang ?? '', detail: `${target?.title ?? selected}\n${request.file.path}`, check: () => service(call).checkWorkspaceImport(selected, request, call.caller.member, call.signal) })
87
+ call.signal.throwIfAborted()
88
+ return service(call).importWorkspace(selected, request, call.caller.member, call.signal)
89
+ },
90
+ },
91
+ ]
@@ -0,0 +1,69 @@
1
+ import { DOCUMENT_ACTION_MAX_REQUEST_CHARS, DOCUMENT_ACTION_MAX_RESPONSE_CHARS } from '@lincyaw/kernel'
2
+ import type { DocumentAuthoringActionEffect, DocumentAuthoringActionPlan, DocumentAuthoringActionRequest, DocumentAuthoringActionReference, DocumentComponentImpactQuery } from '@lincyaw/kernel'
3
+ import { DOCUMENT_AUTHORING_ACTION_SCHEMA, DOCUMENT_AUTHORING_ACTION_APPLY_SCHEMA, DOCUMENT_AUTHORING_ACTION_IMPACT_SCHEMA } from '@lincyaw/kernel/node'
4
+ import type { Call, ToolSpec } from './mcp.ts'
5
+
6
+ function service(call: Call) {
7
+ if (!call.host.authoring) throw Object.assign(new Error('authoring-host-unavailable'), { status: 503 })
8
+ return call.host.authoring
9
+ }
10
+ function schema(source: unknown): Record<string, unknown> {
11
+ const { $schema: _schema, $defs, ...request } = structuredClone(source) as Record<string, unknown>
12
+ return { type: 'object', additionalProperties: false, properties: { document: { type: 'string', maxLength: 200 }, request }, required: ['request'], ...$defs ? { $defs } : {} }
13
+ }
14
+ async function bounded(call: Call, work: () => Promise<unknown>) {
15
+ if (JSON.stringify(call.args).length > DOCUMENT_ACTION_MAX_REQUEST_CHARS) throw Object.assign(new Error('authoring-action-request-too-large'), { status: 413, detail: { limits: { maxRequestChars: DOCUMENT_ACTION_MAX_REQUEST_CHARS }, next: 'Submit only changed fields; keep the local draft when an edited value exceeds the limit.' } })
16
+ try {
17
+ const value = await work()
18
+ if (JSON.stringify(value, null, 2).length > DOCUMENT_ACTION_MAX_RESPONSE_CHARS) throw Object.assign(new Error('authoring-action-response-too-large'), { status: 413 })
19
+ return value
20
+ } catch (error) {
21
+ const failure = error as { detail?: unknown }
22
+ if (failure.detail && JSON.stringify(failure.detail).length > 2048) failure.detail = { truncated: true, next: 'Read complete canonical entities with read_document_part, then explicitly retry from the current source.' }
23
+ throw error
24
+ }
25
+ }
26
+ function effectSummary(effect: DocumentAuthoringActionEffect) {
27
+ if (effect.kind !== 'component') return effect
28
+ const { paths, affected, overrides, references, ...target } = effect.value
29
+ const counts = (value: Record<string, string[]>) => Object.fromEntries(Object.entries(value).map(([name, values]) => [name, values.length]))
30
+ return { kind: 'component', value: { ...target, paths: paths.length, affected: counts(affected), overrides: counts(overrides), references: counts(references) } }
31
+ }
32
+ function summary(plan: DocumentAuthoringActionPlan, document: string) {
33
+ const resource = (part: string) => `presmium://authoring-action/${encodeURIComponent(plan.planId)}/${plan.digest}/${part}?document=${encodeURIComponent(document)}`
34
+ return { apiVersion: plan.apiVersion, planId: plan.planId, changeId: plan.changeId, source: plan.source, digest: plan.digest, expiresAt: plan.expiresAt,
35
+ action: plan.action.type, effect: effectSummary(plan.effect), confirmationRequired: plan.action.type === 'comment.delete',
36
+ impact: Object.fromEntries(Object.entries(plan.impact).map(([key, value]) => [key, Array.isArray(value) ? value.length : value])),
37
+ validation: { errors: plan.issues.filter(issue => issue.severity === 'error').length, warnings: plan.issues.filter(issue => issue.severity === 'warning').length,
38
+ sample: plan.issues.slice(0, 3).map(issue => ({ code: issue.code.slice(0, 64), path: issue.path.slice(0, 100), message: issue.message.slice(0, 120), severity: issue.severity })) },
39
+ resources: plan.effect.kind === 'component' ? { impact: resource('impact/affected.steps/0') } : { candidate: resource('candidate'), details: resource('details') },
40
+ next: plan.action.type === 'comment.delete' ? 'Review this exact deletion and apply the unchanged reference with confirmDeletion:true.' : plan.effect.kind === 'component' ? 'Read the actual inherited-step and Definition-reference impact, apply this unchanged reference, then read the target with the returned saved source.' : 'Apply this unchanged plan reference, then read the target with the returned saved source. Voice settings are not synthesis or recording adoption.' }
41
+ }
42
+ export const AUTHORING_ACTION_TOOLS: readonly ToolSpec[] = [
43
+ {
44
+ name: 'plan_authoring_action', kind: 'read', structured: true,
45
+ description: 'Preflights canonical comments, voice.set, manual caption timing or component actions through shared pure Web rules. Use exact source and a fresh intent id. Components support component.parameters.patch for base values, component.step-parameters.patch/reset for a stable step and component.definition.use for an existing Definition. Parameter changes are bounded set/remove path arrays for schema-declared editable fields; read-only ancestors/descendants and undeclared changes are refused. Whole-object edits preserve unknown fields. Exact whole-array reorders or item deletion are supported; ambiguous mixed reorder/edit must be split. This is not a whole-document or whole-parameters echo; arrays/objects are valid values and existing array items may be changed or removed. Preserve unknown parameters and revalidate the complete inherited graph. Step edits can affect descendants; read the returned actual impact, never claim only this step. Independent implementation copies are unavailable. Comment content is canonical RichTextNode[]; the service fixes IDs, trusted authors and dates. Voice accepts tts/none/null; recording requires adopt_recording. caption.timing.set uses the current stable Step and its read_production_plan captionTiming basis; from/to partition the entire ordered canonical narration display in UTF16 grapheme boundaries, and startMs/endMs are non-overlapping author times relative to that confirmed step. Real audio is required; cross-step speech is unavailable. This creates manual segment times, never word alignment. caption.timing.reset returns to the automatic projection. The service rechecks real cache-only timing at preflight/apply; stale bases cannot rebind implicitly. Returns compact counts/reference and a bounded impact resource, no scene compile or provider. Request<=4096 JSON chars, result<=7168 pretty JSON UTF16 chars.',
46
+ inputSchema: schema(DOCUMENT_AUTHORING_ACTION_SCHEMA),
47
+ run: call => bounded(call, async () => { const document = await call.document(); return summary(await service(call).planAction(document, call.args.request as DocumentAuthoringActionRequest, call.caller.member), document) }),
48
+ },
49
+ {
50
+ name: 'apply_authoring_action', kind: 'operate', structured: true,
51
+ description: 'Applies only the unchanged planId/source/digest returned by plan_authoring_action. Comment deletion also requires confirmDeletion:true after reviewing that exact plan. Reuses the original authoring transaction, member/doc scope, CAS, review gate, history, bounded receipts and exact saved-candidate readback. Other plan families are refused. Retry this exact reference after lost ACK; stale/expired/changed/unknown plans cannot silently replan or reallocate IDs. Saved voice configuration is not generation, provider capability, playback or validated recording adoption.',
52
+ inputSchema: schema(DOCUMENT_AUTHORING_ACTION_APPLY_SCHEMA),
53
+ run: call => bounded(call, async () => {
54
+ const document = await call.document(), own = service(call), capability = own.capabilities(document, call.caller.member)
55
+ if (!capability.actions.applyAction) throw Object.assign(new Error(capability.applyUnavailableReason === 'review-required' ? 'authoring-review-required' : 'authoring-permission-denied'), { status: capability.applyUnavailableReason ? 409 : 403 })
56
+ const entry = (await call.host.documents()).find(item => item.dir === document)
57
+ await call.approve('operate', messages => messages.applyAuthoringAction, { lang: entry?.lang ?? '', check: () => own.checkAction(document, call.args.request as DocumentAuthoringActionReference, call.caller.member) })
58
+ call.signal.throwIfAborted()
59
+ const answer = await own.applyAction(document, call.args.request as DocumentAuthoringActionReference, call.caller.member)
60
+ return { ...answer, effect: effectSummary(answer.effect) }
61
+ }),
62
+ },
63
+ {
64
+ name: 'read_authoring_action_impact', kind: 'read', structured: true,
65
+ description: 'Reads a bounded page of the fixed component plan’s actual affected blocks/steps/pages, preserved/materialized/reset override StepIDs or before/after Definition references. Use its exact planId/digest, collection and nextOffset. Returns plan source plus current source/state; an old plan is not permission to apply. No compilation or new plan. The same retained plan, member/document scope and expiry rules apply; this creates no snapshot ledger.',
66
+ inputSchema: schema(DOCUMENT_AUTHORING_ACTION_IMPACT_SCHEMA),
67
+ run: call => bounded(call, async () => service(call).readActionImpact(await call.document(), call.args.request as DocumentComponentImpactQuery, call.caller.member)),
68
+ },
69
+ ]
@@ -0,0 +1,34 @@
1
+ import { AUTHORING_CLIENT_MAX_REQUEST_CHARS, AUTHORING_CLIENT_MAX_RESPONSE_CHARS, AUTHORING_CLIENT_QUERY_SCHEMA, AUTHORING_CLIENT_REQUEST_QUERY_SCHEMA, AUTHORING_CLIENT_REQUEST_SCHEMA } from '@lincyaw/kernel'
2
+ import { validateAuthoringClientRequest } from '@lincyaw/kernel/node'
3
+ import type { AuthoringClients } from '@lincyaw/kernel'
4
+ import type { Call, ToolSpec } from './mcp.ts'
5
+
6
+ const documentProperty = { type: 'string', maxLength: 200, description: 'Served document directory; empty string means the root document.' }
7
+ const readSchema = (schema: typeof AUTHORING_CLIENT_QUERY_SCHEMA) => ({ ...schema, properties: { document: documentProperty, ...schema.properties as object } })
8
+ const service = (call: Call): AuthoringClients => { if (!call.host.authoringClients) throw Object.assign(new Error('authoring-client-host-unavailable'), { status: 503 }); return call.host.authoringClients }
9
+ const query = (call: Call) => { const { document: _document, ...value } = call.args; return value }
10
+ async function bounded(call: Call, run: () => Promise<unknown>) {
11
+ if (JSON.stringify(call.args).length > AUTHORING_CLIENT_MAX_REQUEST_CHARS) throw Object.assign(new Error('client-request-too-large'), { status: 413 })
12
+ const answer = await run()
13
+ if (JSON.stringify(answer, null, 2).length > AUTHORING_CLIENT_MAX_RESPONSE_CHARS) throw Object.assign(new Error('client-response-too-large'), { status: 413 })
14
+ return answer
15
+ }
16
+ export const AUTHORING_CLIENT_TOOLS: ToolSpec[] = [
17
+ { name: 'get_authoring_clients', kind: 'read', structured: true, inputSchema: readSchema(AUTHORING_CLIENT_QUERY_SCHEMA),
18
+ description: 'Reads bounded last reported state of actual Studio document clients, scoped to this document and member. No editor text, drafts, credentials or server-estimated playhead. Inspect connected, epoch, connectionId, snapshot.source/revision and actual actions; offline reports are observations, never current capability. Use nextAfter for additional clients. No clients means unavailable. No compilation, open-document or UI mode change. Request<=4096 JSON chars; result<=7168 pretty UTF16 chars.',
19
+ run: call => bounded(call, async () => service(call).list(await call.document(), query(call), call.caller.member)) },
20
+ { name: 'request_authoring_client', kind: 'operate', structured: true,
21
+ inputSchema: { type: 'object', additionalProperties: false, properties: { document: documentProperty, request: AUTHORING_CLIENT_REQUEST_SCHEMA }, required: ['request'] },
22
+ description: 'Explicitly requests one advertised browser action with exact clientId/epoch, CanonicalSource, connectionId/revision and intent requestId. Requires operate approval. Returns sent admission, never completed execution; poll get_authoring_client_request and inspect state, verification and current. Only a browser-owned channel can ACK. preview.prepare explicitly compiles/prepares the current clean saved source, pauses first, and preserves mode/position; no synthesis or playback. Its real prepared source/IR revision is in snapshot.preview. Input, a public modal, document switch, disconnect, Escape or the 30s request deadline cancels adoption; the human prepare default remains 45s. Playback needs an already valid exact plan/binding and real frame acknowledgement; other actions never auto-compile or enter Video/Present. source.flush flushes only canonical source edits, not comment/timing/recording form drafts. comment-drafts.inspect explicitly returns only comment-backup metadata; comment-drafts.read explicitly returns version/token-bound UTF-16 JSON-text fragments with UTF-8 SHA-256 and lengths. Assemble all fragments before parsing. component-drafts.inspect explicitly returns component-property backup metadata only, with target/version/storage, current (this client bank versus a backup) and sourceCurrent. component-drafts.read requires its exact id/revision/token/blockId/optional stepId reference and returns only the parameter value as presmium-component-parameters-json-v1 fragments, including current session-only input. Baselines, scope identities and pending internals are never exposed. The inspect/read actions do not mutate drafts. component-drafts.restore requires an exact backup reference plus expectedCurrent (the exact current reference or null), strictly forks before adopting and preserves the current branch; pending/session-only/IME input or an unavailable target is refused. component-drafts.discard requires the exact reference and current boolean, retiring only that current revision or backup without retiring its unselected origins. comment-drafts.restore is available only for an already mounted exact native reply/edit composer advertised by inspect (composerId + expectedCurrent reference/null); it strictly forks and verifies native editor adoption, without submitting a comment or changing document selection. New-comment composer restoration remains unavailable. comment-drafts.discard retires only an exact backup with no active native composer for that target; active/pending/IME cases require the human owner. These actions do not apply parameters to canonical source, rebase stale drafts, navigate, focus or flush forms. Use the human properties Apply only after reviewing the restored draft. Cross-window state is observed, not globally atomic. Default client listings and passive reports never include draft content. Draft changes invalidate client revision and old references; old successful receipts are historical when current=false. No complete local form/draft inspection is claimed. Read state again after any refused/failed/expired result. Identical intent retries return retained receipts without re-execution (64 process-local, same epoch); changed intent, stale source/state, disconnect or replaced epoch is refused. Never retry play or prepare with a new ID just because an ACK was lost.',
23
+ run: call => bounded(call, async () => {
24
+ const document = await call.document(), request = call.args.request
25
+ if (!validateAuthoringClientRequest(request)) throw Object.assign(new Error('client-request-invalid'), { status: 400 })
26
+ const lang = (await call.host.documents()).find(item => item.dir === document)?.lang ?? 'zh-CN'
27
+ await call.approve('operate', messages => messages.authoringClientAction(request.action.type), { lang })
28
+ call.signal.throwIfAborted()
29
+ return service(call).request(document, request, call.caller.member)
30
+ }) },
31
+ { name: 'get_authoring_client_request', kind: 'read', structured: true, inputSchema: readSchema(AUTHORING_CLIENT_REQUEST_QUERY_SCHEMA),
32
+ description: 'Reads this exact retained browser request receipt: sent/executing/done/refused/failed/expired. HTTP admission is not success. browser-acknowledged means an actual client observation; current additionally checks that client epoch/connection/state still matches. A stale completed receipt never describes a newly opened document. No action is re-dispatched by reading. Read the client again for subsequent commands; recording, new-comment restore, active-comment discard and other unwired draft families cannot be inferred from these explicit comment/component reads or playback.',
33
+ run: call => bounded(call, async () => service(call).get(await call.document(), query(call) as { clientId: string, epoch: string, requestId: string }, call.caller.member)) },
34
+ ]