openpencil/packages/mcp/src/http.ts
Danila Poyarkov f3eac5ac8b Lint all packages: fix 193 errors across core, mcp, acp
Extend lint coverage from src/ to all packages (178 files, 140 rules).

Core (37 files):
- Remove 98 unnecessary conditions (dead ?./?? on non-nullish values)
- Remove 38 unnecessary type assertions (! and as casts)
- Convert 12 patterns to optional chain (a && a.b → a?.b)
- Fix 10 consistent-type-imports (merge duplicates, inline type keyword)
- Add parentheses to 16 nested ternaries
- Convert 4 indexed for-loops to for-of
- Merge 4 duplicate import statements

MCP (3 files):
- Replace typeof undefined check with 'in' operator
- Capture graph in local const to avoid ! assertions

ACP (1 file):
- Fix 5 TypeScript errors (type predicates, object shapes, dead code)
- Remove unnecessary template expression

Vendored kiwi-schema gets broader rule overrides to avoid touching it.
Core complexity stays as warning (24 functions over limit).
2026-03-09 13:18:08 +03:00

99 lines
3.1 KiB
JavaScript

#!/usr/bin/env node
import { randomUUID } from 'node:crypto'
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js'
import { createServer } from './server.js'
const pkg = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf-8'))
const port = parseInt(process.env.PORT ?? '3100', 10)
const host = process.env.HOST ?? '127.0.0.1'
const authToken = process.env.OPENPENCIL_MCP_AUTH_TOKEN?.trim() || null
const corsOrigin = process.env.OPENPENCIL_MCP_CORS_ORIGIN?.trim() || null
const fileRoot = resolve(process.env.OPENPENCIL_MCP_ROOT ?? process.cwd())
const sessions = new Map<string, { server: ReturnType<typeof createServer>; transport: WebStandardStreamableHTTPServerTransport }>()
async function getOrCreateSession(sessionId?: string) {
if (sessionId && sessions.has(sessionId)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guarded by has() above
return sessions.get(sessionId)!
}
const id = sessionId ?? randomUUID()
const server = createServer(pkg.version, {
enableEval: false,
fileRoot
})
const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: () => id })
await server.connect(transport)
sessions.set(id, { server, transport })
return { server, transport }
}
const app = new Hono()
if (corsOrigin) {
app.use('*', cors({
origin: corsOrigin,
allowMethods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
allowHeaders: [
'Content-Type',
'Authorization',
'x-mcp-token',
'mcp-session-id',
'Last-Event-ID',
'mcp-protocol-version'
],
exposeHeaders: ['mcp-session-id', 'mcp-protocol-version']
}))
}
app.get('/health', (c) =>
c.json({
status: 'ok',
version: pkg.version,
authRequired: Boolean(authToken),
evalEnabled: false,
fileRoot
})
)
app.all('/mcp', async (c) => {
if (authToken) {
const authHeader = c.req.header('authorization')
const token =
authHeader?.startsWith('Bearer ')
? authHeader.slice('Bearer '.length)
: c.req.header('x-mcp-token')
if (token !== authToken) {
return c.json({ error: 'Unauthorized' }, 401)
}
}
const sessionId = c.req.header('mcp-session-id') ?? undefined
const { transport } = await getOrCreateSession(sessionId)
return transport.handleRequest(c.req.raw)
})
const isBun = 'Bun' in globalThis
if (isBun) {
Bun.serve({ fetch: app.fetch, port, hostname: host })
} else {
const { serve } = await import('@hono/node-server')
serve({ fetch: app.fetch, port, hostname: host })
}
console.log(`OpenPencil MCP server v${pkg.version}`)
console.log(` Health: http://${host}:${port}/health`)
console.log(` MCP: http://${host}:${port}/mcp`)
console.log(` Auth: ${authToken ? 'required (OPENPENCIL_MCP_AUTH_TOKEN)' : 'disabled'}`)
console.log(` CORS: ${corsOrigin ?? 'disabled'}`)
console.log(` Eval: disabled`)
console.log(` Root: ${fileRoot}`)