openpencil/packages/mcp/src/index.ts
Vitali sharp8n c6a65c4878 feat(web): ship the Vue editor web build with online font providers
- Add Dockerfile.web-vue + docker/ container: static SPA on :3100 and the
  MCP Streamable-HTTP server on :7600 in one image, replacing the retired
  Rust web host used by the w4c iframe integration
- Allow online font providers (Google Fonts, Fontsource, Bunny, Fontshare)
  to load in the browser via native CORS-enabled fetch instead of gating
  them on the Tauri fetch proxy
- Rename webFontProvidersRequireDesktopApp to webFontLoadFailed and reword
  the toast for connection failures; drop the unconditional toast from the
  font family picker
- Support OPENPENCIL_MCP_HOST so the MCP server binds 0.0.0.0 in containers
2026-08-22 00:33:54 +03:00

110 lines
5.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
import { startServer } from '#mcp/server'
if (process.argv.includes('--help') || process.argv.includes('-h')) {
process.stdout.write(
`openpencil-mcp-http\n\n` +
`Start the OpenPencil MCP server.\n\n` +
`On macOS/Linux, the server listens on a Unix domain socket by default\n` +
`with optional TCP for browser clients. On Windows, only TCP is available.\n\n` +
`Options:\n` +
` --help, -h Show this help message\n\n` +
`Environment variables:\n` +
` PORT TCP port (default: 7600, set to 0 to disable TCP)\n` +
` OPENPENCIL_MCP_SOCKET Override Unix socket path (recorded in the discovery file)\n` +
` OPENPENCIL_MCP_DISCOVERY_PATH Override discovery file (mcp.json) location; defaults to the\n` +
` platform path. Parent dir created 0o700. Mainly for test isolation.\n` +
` OPENPENCIL_MCP_TCP Deprecated — TCP is controlled by PORT (>0 = on, 0 = off)\n` +
` OPENPENCIL_MCP_AUTH_TOKEN Bearer token for MCP and RPC auth\n` +
` OPENPENCIL_MCP_HOST TCP bind host (default: 127.0.0.1; set 0.0.0.0 in containers)\n` +
` OPENPENCIL_MCP_ROOT Allowed directory for file-scoped tools (default: current working directory)\n` +
` OPENPENCIL_MCP_EVAL Set to 1 to enable the eval tool\n` +
` OPENPENCIL_MCP_CORS_ORIGIN Allowed CORS origin\n` +
` OPENPENCIL_MCP_APP_TIMEOUT_MS If set, close the server and remove its discovery\n` +
` file after no app is attached for this many ms. The\n` +
` grace period starts at startup and after disconnects.\n` +
` Unset/0 disables it (default) — do not set this for\n` +
` manual/CLI use, since nothing may ever register.\n`
)
process.exit(0)
}
const rawPortText = (process.env.PORT ?? '7600').trim()
// Reject non-digit strings (including hex like "0x50" and partially-numeric like
// "7600abc") — Number.parseInt would silently parse these, masking misconfig.
if (!/^\d+$/.test(rawPortText)) {
process.stderr.write(`Error: PORT must be an integer in 065535, got "${process.env.PORT}"\n`)
process.exit(1)
}
const rawPort = Number.parseInt(rawPortText, 10)
// Validate PORT: must be an integer in 065535. 0 means "disable TCP".
if (rawPort < 0 || rawPort > 65535) {
process.stderr.write(`Error: PORT must be an integer in 065535, got "${process.env.PORT}"\n`)
process.exit(1)
}
const port = rawPort
// OPENPENCIL_MCP_TCP is accepted for backward compat but has no effect —
// the PORT value alone determines whether TCP is enabled (PORT=0 is the kill switch).
const withTcp = port > 0
const MAX_APP_TIMEOUT_MS = 2_147_483_647
const rawAppTimeoutText = process.env.OPENPENCIL_MCP_APP_TIMEOUT_MS?.trim()
let appAttachTimeoutMs: number | undefined
if (rawAppTimeoutText) {
if (!/^\d+$/.test(rawAppTimeoutText)) {
process.stderr.write(
`Error: OPENPENCIL_MCP_APP_TIMEOUT_MS must be a non-negative integer, got "${rawAppTimeoutText}"\n`
)
process.exit(1)
}
appAttachTimeoutMs = Number.parseInt(rawAppTimeoutText, 10)
if (!Number.isSafeInteger(appAttachTimeoutMs) || appAttachTimeoutMs > MAX_APP_TIMEOUT_MS) {
process.stderr.write(
`Error: OPENPENCIL_MCP_APP_TIMEOUT_MS must be an integer in 0${MAX_APP_TIMEOUT_MS}, got "${rawAppTimeoutText}"\n`
)
process.exit(1)
}
}
const handle = await startServer({
httpPort: withTcp ? port : 0,
withTcp,
socketPath: process.env.OPENPENCIL_MCP_SOCKET?.trim() || null,
tcpHost: process.env.OPENPENCIL_MCP_HOST?.trim() || '127.0.0.1',
enableEval: process.env.OPENPENCIL_MCP_EVAL === '1',
mcpRoot: process.env.OPENPENCIL_MCP_ROOT?.trim() || process.cwd(),
// Auth token: undefined → auto-generate, empty string → disable auth,
// non-empty → use trimmed value. Whitespace-only is rejected to prevent a
// silent fallback to an auto-generated token when the operator intended to
// set an explicit one.
authToken: (() => {
const raw = process.env.OPENPENCIL_MCP_AUTH_TOKEN
if (raw === undefined) return undefined
if (raw === '') return null
const trimmed = raw.trim()
if (!trimmed) {
process.stderr.write(
'Error: OPENPENCIL_MCP_AUTH_TOKEN is whitespace-only. Set a real token, or use an empty string to disable auth.\n'
)
process.exit(1)
}
return trimmed
})(),
corsOrigin: process.env.OPENPENCIL_MCP_CORS_ORIGIN?.trim() || null,
appAttachTimeoutMs
})
process.stderr.write(`OpenPencil MCP server\n`)
if (handle.socketPath) process.stderr.write(` Socket: ${handle.socketPath}\n`)
if (handle.httpPort) process.stderr.write(` HTTP: http://127.0.0.1:${handle.httpPort}\n`)
// Graceful shutdown on signals
const shutdown = async () => {
process.stderr.write('\nShutting down MCP server...\n')
await handle.close()
process.exit(0)
}
process.on('SIGINT', () => void shutdown().catch(() => process.exit(1)))
process.on('SIGTERM', () => void shutdown().catch(() => process.exit(1)))