@lincyaw/mcp 0.0.0-stage → 1.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/stdio.ts ADDED
@@ -0,0 +1,101 @@
1
+ import { stat } from 'node:fs/promises'
2
+ import { dirname } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { parseArgs } from 'node:util'
5
+ import { errorMessage } from '@lincyaw/kernel'
6
+ import type { ApprovalKind, Member } from '@lincyaw/kernel'
7
+ import { buildView, Compiler, findPackageDir, resolveEntry, runBlock } from '@lincyaw/compiler'
8
+ import type { Compilation } from '@lincyaw/compiler'
9
+ import type { McpHost } from './host.ts'
10
+ import { serveMcpStdio } from './mcp.ts'
11
+
12
+ const AUTHOR: Member = { id: 'local', name: 'local author', roles: ['owner'] }
13
+ const KINDS: readonly ApprovalKind[] = ['operate', 'spend', 'write', 'agent']
14
+ const OWN_DIR = fileURLToPath(new URL('..', import.meta.url))
15
+ const CLI_PACKAGE = '@lincyaw/cli'
16
+
17
+ const USAGE = `Usage: presmium-mcp [deck] [--allow <kinds>]
18
+
19
+ Serves the deck (a directory holding lecture.md, or an entry file; default: .)
20
+ as MCP tools and resources on stdio, for the agent of an editor.
21
+
22
+ --allow <kinds> offer and allow these operations: operate (run_code, build), spend
23
+ -h, --help show this help`
24
+
25
+ async function stamps(files: string[]): Promise<string> {
26
+ const times = await Promise.all(files.map(file => stat(file).then(s => s.mtimeMs, () => -1)))
27
+ return times.join(',')
28
+ }
29
+
30
+ export function compilerHost(entry: string, allow: readonly ApprovalKind[]): McpHost {
31
+ const bundled = findPackageDir(CLI_PACKAGE, dirname(entry)) ?? findPackageDir(CLI_PACKAGE, OWN_DIR) ?? OWN_DIR
32
+ const compiler = new Compiler(entry, { bundled })
33
+ let latest: { compilation: Compilation, files: string[], stamp: string } | undefined
34
+ let queue = Promise.resolve()
35
+ const current = () => {
36
+ const next = queue.then(async () => {
37
+ if (latest && await stamps(latest.files) === latest.stamp) return latest.compilation
38
+ const compilation = await compiler.compile()
39
+ const files = [...compilation.files, ...compilation.dependencies.files]
40
+ latest = { compilation, files, stamp: await stamps(files) }
41
+ return compilation
42
+ })
43
+ queue = next.then(() => undefined, () => undefined)
44
+ return next
45
+ }
46
+ const only = async (document: string) => {
47
+ if (document !== '') throw new Error(`no document ${JSON.stringify(document)}: presmium-mcp serves one deck`)
48
+ return current()
49
+ }
50
+ return {
51
+ process: { command: 'presmium-mcp' },
52
+ async documents() {
53
+ const { doc } = await current()
54
+ return [{ dir: '', title: doc.meta.title, lang: doc.meta.lang }]
55
+ },
56
+ async compile(document) {
57
+ const { doc } = await only(document)
58
+ return { doc, diagnostics: doc.diagnostics }
59
+ },
60
+ async check(document) {
61
+ const compilation = await only(document)
62
+ return [...compilation.doc.diagnostics, ...await compilation.check()]
63
+ },
64
+ async build(document, options) {
65
+ const report = await buildView(await only(document), { to: options.to })
66
+ return { outputs: report.outputs.map(({ renderer, outDir }) => ({ renderer, outDir })), findings: report.findings }
67
+ },
68
+ run: async (document, target, signal) => runBlock(await only(document), target, signal),
69
+ approve: async request => allow.includes(request.kind),
70
+ offers: kind => allow.includes(kind),
71
+ async tools() {
72
+ const compilation = await current()
73
+ return { tools: compilation.plugins.flatMap(loaded => loaded.plugin?.tools ?? []), service: compilation.service }
74
+ },
75
+ }
76
+ }
77
+
78
+ export async function main(argv: string[]): Promise<number | undefined> {
79
+ let deck: string
80
+ let allow: ApprovalKind[]
81
+ try {
82
+ const { values, positionals } = parseArgs({ args: argv, allowPositionals: true, options: { allow: { type: 'string' }, help: { type: 'boolean', short: 'h' } } })
83
+ if (values.help) {
84
+ process.stdout.write(`${USAGE}\n`)
85
+ return 0
86
+ }
87
+ if (positionals.length > 1) throw new Error(`unexpected argument "${positionals[1]}"`)
88
+ deck = resolveEntry(positionals[0] ?? '.')
89
+ allow = (values.allow?.split(',').map(kind => kind.trim()).filter(Boolean) ?? []) as ApprovalKind[]
90
+ const unknown = allow.find(kind => !KINDS.includes(kind))
91
+ if (unknown) throw new Error(`--allow takes ${KINDS.join(', ')}, not "${unknown}"`)
92
+ await stat(deck).catch(() => {
93
+ throw new Error(`${deck} not found`)
94
+ })
95
+ } catch (error) {
96
+ process.stderr.write(`presmium-mcp: ${errorMessage(error)}\n\n${USAGE}\n`)
97
+ return 2
98
+ }
99
+ serveMcpStdio(compilerHost(deck, allow), { member: AUTHOR, mode: 'author' })
100
+ return undefined
101
+ }