import type { VercelRequest, VercelResponse } from '@vercel/node'
import { Tool } from '@modelcontextprotocol/sdk/types.js'
import { randomUUID } from 'node:crypto'
import { kv } from '@vercel/kv'
import { chromium as playwright, Browser, Page, BrowserContext } from 'playwright-core'
// Browserless.io configuration
const BROWSERLESS_URL = process.env.BROWSERLESS_URL // e.g., wss://chrome.browserless.io?token=YOUR_TOKEN
const BROWSERLESS_TOKEN = process.env.BROWSERLESS_TOKEN
const authTokens = [process.env.MCP_AUTH_TOKEN, process.env.AUTH_TOKEN].filter(
(t): t is string => typeof t === 'string' && t.length > 0
)
const requireAuth =
process.env.REQUIRE_AUTH === 'true' ||
(process.env.REQUIRE_AUTH !== 'false' && process.env.VERCEL_ENV === 'production')
// Session data interface
interface BrowserSession {
id: string
url: string
cookies: Array<{
name: string
value: string
domain: string
path: string
expires?: number
httpOnly?: boolean
secure?: boolean
sameSite?: 'Strict' | 'Lax' | 'None'
}>
localStorage: Record<string, string>
lastSnapshot?: string
refs?: Record<string, { role: string; name: string; selector: string }>
httpCredentials?: { username: string; password: string }
createdAt: number
updatedAt: number
}
const SESSION_TTL = 15 * 60 // 15 minutes
// Session management functions
async function getSession(sessionId: string): Promise<BrowserSession | null> {
try {
const session = await kv.get<BrowserSession>(`browser_session:${sessionId}`)
return session
} catch (error) {
console.error('Failed to get session:', error)
return null
}
}
async function saveSession(session: BrowserSession): Promise<void> {
try {
await kv.set(`browser_session:${session.id}`, session, { ex: SESSION_TTL })
} catch (error) {
console.error('Failed to save session:', error)
}
}
async function deleteSession(sessionId: string): Promise<void> {
try {
await kv.del(`browser_session:${sessionId}`)
} catch (error) {
console.error('Failed to delete session:', error)
}
}
// Browser management
async function launchBrowser(): Promise<Browser> {
// Use Browserless.io cloud browser if configured
if (BROWSERLESS_TOKEN) {
const browserlessWsUrl = `wss://production-sfo.browserless.io/chromium/playwright?token=${BROWSERLESS_TOKEN}`
console.log('Connecting to Browserless cloud browser via Playwright...')
const browser = await playwright.connect(browserlessWsUrl)
return browser
}
if (BROWSERLESS_URL) {
console.log('Connecting to custom browser endpoint...')
const browser = await playwright.connect(BROWSERLESS_URL)
return browser
}
// Fallback to local chromium (for local development)
throw new Error('BROWSERLESS_URL or BROWSERLESS_TOKEN environment variable is required for cloud browser. Get a free API key at https://browserless.io')
}
async function setupPage(
browser: Browser,
session: BrowserSession | null,
httpCredentials?: { username: string; password: string }
): Promise<{ context: BrowserContext; page: Page }> {
// Use credentials from parameter, session, or none
const credentials = httpCredentials || session?.httpCredentials
const context = await browser.newContext({
viewport: { width: 1280, height: 720 },
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
httpCredentials: credentials,
})
// Restore cookies if session exists
if (session?.cookies && session.cookies.length > 0) {
await context.addCookies(session.cookies)
}
const page = await context.newPage()
// Restore localStorage if session exists and has a URL
if (session?.localStorage && session.url && Object.keys(session.localStorage).length > 0) {
await page.goto(session.url, { waitUntil: 'domcontentloaded' })
await page.evaluate((storage) => {
for (const [key, value] of Object.entries(storage)) {
localStorage.setItem(key, value)
}
}, session.localStorage)
}
return { context, page }
}
async function extractSessionData(
context: BrowserContext,
page: Page,
sessionId: string,
refs?: Record<string, { role: string; name: string; selector: string }>,
httpCredentials?: { username: string; password: string }
): Promise<BrowserSession> {
const cookies = await context.cookies()
const url = page.url()
let localStorage: Record<string, string> = {}
try {
localStorage = await page.evaluate(() => {
const storage: Record<string, string> = {}
for (let i = 0; i < window.localStorage.length; i++) {
const key = window.localStorage.key(i)
if (key) {
storage[key] = window.localStorage.getItem(key) || ''
}
}
return storage
})
} catch {
// localStorage might not be available on some pages
}
return {
id: sessionId,
url,
cookies: cookies.map(c => ({
name: c.name,
value: c.value,
domain: c.domain,
path: c.path,
expires: c.expires,
httpOnly: c.httpOnly,
secure: c.secure,
sameSite: c.sameSite as 'Strict' | 'Lax' | 'None' | undefined,
})),
localStorage,
refs,
httpCredentials,
createdAt: Date.now(),
updatedAt: Date.now(),
}
}
// Accessibility snapshot helper
async function getAccessibilitySnapshot(page: Page): Promise<{ snapshot: string; refs: Record<string, { role: string; name: string; selector: string }> }> {
const snapshot = await page.accessibility.snapshot({ interestingOnly: true })
const refs: Record<string, { role: string; name: string; selector: string }> = {}
let refCounter = 1
function processNode(node: any, path: string[] = []): string {
if (!node) return ''
const lines: string[] = []
const indent = ' '.repeat(path.length)
const role = node.role || 'unknown'
const name = node.name || ''
// Generate ref for interactive elements
let refStr = ''
if (['button', 'link', 'textbox', 'checkbox', 'radio', 'combobox', 'menuitem', 'tab'].includes(role)) {
const refId = `e${refCounter++}`
refs[refId] = {
role,
name,
selector: generateSelector(role, name),
}
refStr = ` [ref=${refId}]`
}
let line = `${indent}- ${role}`
if (name) line += ` "${name}"`
line += refStr
lines.push(line)
if (node.children) {
for (const child of node.children) {
const childOutput = processNode(child, [...path, role])
if (childOutput) lines.push(childOutput)
}
}
return lines.join('\n')
}
function generateSelector(role: string, name: string): string {
if (name) {
return `role=${role}[name="${name.replace(/"/g, '\\"')}"]`
}
return `role=${role}`
}
const snapshotText = processNode(snapshot)
return { snapshot: snapshotText, refs }
}
// Tool definitions
const tools: Tool[] = [
{
name: 'browser_open',
description: 'URLを開いてブラウザセッションを開始します。セッションIDが返されるので、以降の操作で使用してください。Basic認証が必要なサイトの場合はbasic_authを指定してください。',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string', description: '開くURL' },
session_id: { type: 'string', description: '既存のセッションID(任意)。指定すると前回のCookies/localStorageを復元します' },
basic_auth: {
type: 'object',
description: 'Basic認証の資格情報(任意)。ユーザー名とパスワードを指定します',
properties: {
username: { type: 'string', description: 'ユーザー名' },
password: { type: 'string', description: 'パスワード' },
},
required: ['username', 'password'],
},
},
required: ['url'],
},
},
{
name: 'browser_snapshot',
description: 'ページのアクセシビリティスナップショットを取得します。要素にはref(@e1, @e2など)が付与され、クリックや入力操作で使用できます。',
inputSchema: {
type: 'object',
properties: {
session_id: { type: 'string', description: 'セッションID' },
url: { type: 'string', description: 'スナップショットを取得するURL(session_idがない場合に使用)' },
},
},
},
{
name: 'browser_click',
description: '要素をクリックします。ref(@e1など)またはCSSセレクタを指定できます。',
inputSchema: {
type: 'object',
properties: {
session_id: { type: 'string', description: 'セッションID' },
url: { type: 'string', description: '操作対象のURL(session_idがない場合に使用)' },
ref: { type: 'string', description: 'スナップショットのref(例: @e1)' },
selector: { type: 'string', description: 'CSSセレクタ(refがない場合に使用)' },
},
},
},
{
name: 'browser_fill',
description: 'フォームフィールドに値を入力します(既存の値を置き換え)。',
inputSchema: {
type: 'object',
properties: {
session_id: { type: 'string', description: 'セッションID' },
url: { type: 'string', description: '操作対象のURL(session_idがない場合に使用)' },
ref: { type: 'string', description: 'スナップショットのref(例: @e1)' },
selector: { type: 'string', description: 'CSSセレクタ(refがない場合に使用)' },
value: { type: 'string', description: '入力する値' },
},
required: ['value'],
},
},
{
name: 'browser_type',
description: 'テキストを1文字ずつ入力します(キーイベントをトリガーする場合に有効)。',
inputSchema: {
type: 'object',
properties: {
session_id: { type: 'string', description: 'セッションID' },
url: { type: 'string', description: '操作対象のURL(session_idがない場合に使用)' },
ref: { type: 'string', description: 'スナップショットのref(例: @e1)' },
selector: { type: 'string', description: 'CSSセレクタ(refがない場合に使用)' },
text: { type: 'string', description: '入力するテキスト' },
submit: { type: 'boolean', description: '入力後にEnterキーを押すか', default: false },
},
required: ['text'],
},
},
{
name: 'browser_get_text',
description: '要素のテキストコンテンツを取得します。',
inputSchema: {
type: 'object',
properties: {
session_id: { type: 'string', description: 'セッションID' },
url: { type: 'string', description: '操作対象のURL(session_idがない場合に使用)' },
ref: { type: 'string', description: 'スナップショットのref(例: @e1)' },
selector: { type: 'string', description: 'CSSセレクタ(refがない場合に使用)' },
},
},
},
{
name: 'browser_screenshot',
description: 'ページのスクリーンショットを取得します(Base64形式)。',
inputSchema: {
type: 'object',
properties: {
session_id: { type: 'string', description: 'セッションID' },
url: { type: 'string', description: 'スクリーンショットを取得するURL(session_idがない場合に使用)' },
full_page: { type: 'boolean', description: 'ページ全体のスクリーンショットを取得するか', default: false },
},
},
},
{
name: 'browser_wait',
description: '指定した条件を待機します。',
inputSchema: {
type: 'object',
properties: {
session_id: { type: 'string', description: 'セッションID' },
url: { type: 'string', description: '操作対象のURL(session_idがない場合に使用)' },
time: { type: 'number', description: '待機する秒数' },
text: { type: 'string', description: '出現を待機するテキスト' },
selector: { type: 'string', description: '出現を待機する要素のセレクタ' },
},
},
},
{
name: 'browser_press_key',
description: 'キーボードのキーを押下します。',
inputSchema: {
type: 'object',
properties: {
session_id: { type: 'string', description: 'セッションID' },
url: { type: 'string', description: '操作対象のURL(session_idがない場合に使用)' },
key: { type: 'string', description: 'キー名(例: Enter, Tab, ArrowDown, Escape)' },
},
required: ['key'],
},
},
{
name: 'browser_close',
description: 'ブラウザセッションを終了し、セッションデータを削除します。',
inputSchema: {
type: 'object',
properties: {
session_id: { type: 'string', description: '終了するセッションID' },
},
required: ['session_id'],
},
},
]
// Tool execution handler
async function executeTool(name: string, args: Record<string, unknown> | undefined): Promise<{ content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>; isError?: boolean }> {
let browser: Browser | null = null
try {
switch (name) {
case 'browser_open': {
const url = args?.url as string
if (!url) throw new Error('url is required')
const existingSessionId = args?.session_id as string | undefined
const basicAuth = args?.basic_auth as { username: string; password: string } | undefined
let session = existingSessionId ? await getSession(existingSessionId) : null
// Use credentials from parameter or existing session
const httpCredentials = basicAuth || session?.httpCredentials
browser = await launchBrowser()
const { context, page } = await setupPage(browser, session, httpCredentials)
await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 })
const sessionId = existingSessionId || randomUUID()
const newSession = await extractSessionData(context, page, sessionId, undefined, httpCredentials)
await saveSession(newSession)
// Capture data before closing browser
const pageUrl = page.url()
const pageTitle = await page.title()
await browser.close()
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
session_id: sessionId,
url: pageUrl,
title: pageTitle,
message: existingSessionId ? 'セッションを復元してページを開きました' : '新しいセッションでページを開きました',
}, null, 2),
}],
}
}
case 'browser_snapshot': {
const sessionId = args?.session_id as string | undefined
const url = args?.url as string | undefined
if (!sessionId && !url) throw new Error('session_id or url is required')
let session = sessionId ? await getSession(sessionId) : null
const targetUrl = url || session?.url
if (!targetUrl) throw new Error('No URL available')
browser = await launchBrowser()
const { context, page } = await setupPage(browser, session, session?.httpCredentials)
await page.goto(targetUrl, { waitUntil: 'networkidle', timeout: 30000 })
const { snapshot, refs } = await getAccessibilitySnapshot(page)
const newSessionId = sessionId || randomUUID()
const newSession = await extractSessionData(context, page, newSessionId, refs, session?.httpCredentials)
newSession.lastSnapshot = snapshot
await saveSession(newSession)
const pageUrl = page.url()
await browser.close()
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
session_id: newSessionId,
url: pageUrl,
snapshot,
refs: Object.fromEntries(
Object.entries(refs).map(([k, v]) => [k, { role: v.role, name: v.name }])
),
}, null, 2),
}],
}
}
case 'browser_click': {
const sessionId = args?.session_id as string | undefined
const url = args?.url as string | undefined
const ref = args?.ref as string | undefined
const selector = args?.selector as string | undefined
if (!sessionId && !url) throw new Error('session_id or url is required')
if (!ref && !selector) throw new Error('ref or selector is required')
let session = sessionId ? await getSession(sessionId) : null
const targetUrl = url || session?.url
if (!targetUrl) throw new Error('No URL available')
browser = await launchBrowser()
const { context, page } = await setupPage(browser, session, session?.httpCredentials)
await page.goto(targetUrl, { waitUntil: 'networkidle', timeout: 30000 })
let targetSelector = selector
if (ref && session?.refs) {
const refKey = ref.startsWith('@') ? ref.slice(1) : ref
const refData = session.refs[refKey]
if (refData) {
targetSelector = refData.selector
}
}
if (!targetSelector) throw new Error('Could not resolve selector')
await page.locator(targetSelector).click({ timeout: 10000 })
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {})
const newSessionId = sessionId || randomUUID()
const newSession = await extractSessionData(context, page, newSessionId, undefined, session?.httpCredentials)
await saveSession(newSession)
const pageUrl = page.url()
await browser.close()
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
session_id: newSessionId,
url: pageUrl,
message: `クリックしました: ${ref || selector}`,
}, null, 2),
}],
}
}
case 'browser_fill': {
const sessionId = args?.session_id as string | undefined
const url = args?.url as string | undefined
const ref = args?.ref as string | undefined
const selector = args?.selector as string | undefined
const value = args?.value as string
if (!value) throw new Error('value is required')
if (!sessionId && !url) throw new Error('session_id or url is required')
if (!ref && !selector) throw new Error('ref or selector is required')
let session = sessionId ? await getSession(sessionId) : null
const targetUrl = url || session?.url
if (!targetUrl) throw new Error('No URL available')
browser = await launchBrowser()
const { context, page } = await setupPage(browser, session, session?.httpCredentials)
await page.goto(targetUrl, { waitUntil: 'networkidle', timeout: 30000 })
let targetSelector = selector
if (ref && session?.refs) {
const refKey = ref.startsWith('@') ? ref.slice(1) : ref
const refData = session.refs[refKey]
if (refData) {
targetSelector = refData.selector
}
}
if (!targetSelector) throw new Error('Could not resolve selector')
await page.locator(targetSelector).fill(value, { timeout: 10000 })
const newSessionId = sessionId || randomUUID()
const newSession = await extractSessionData(context, page, newSessionId, undefined, session?.httpCredentials)
await saveSession(newSession)
const pageUrl = page.url()
await browser.close()
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
session_id: newSessionId,
url: pageUrl,
message: `入力しました: ${ref || selector}`,
}, null, 2),
}],
}
}
case 'browser_type': {
const sessionId = args?.session_id as string | undefined
const url = args?.url as string | undefined
const ref = args?.ref as string | undefined
const selector = args?.selector as string | undefined
const text = args?.text as string
const submit = args?.submit as boolean
if (!text) throw new Error('text is required')
if (!sessionId && !url) throw new Error('session_id or url is required')
if (!ref && !selector) throw new Error('ref or selector is required')
let session = sessionId ? await getSession(sessionId) : null
const targetUrl = url || session?.url
if (!targetUrl) throw new Error('No URL available')
browser = await launchBrowser()
const { context, page } = await setupPage(browser, session, session?.httpCredentials)
await page.goto(targetUrl, { waitUntil: 'networkidle', timeout: 30000 })
let targetSelector = selector
if (ref && session?.refs) {
const refKey = ref.startsWith('@') ? ref.slice(1) : ref
const refData = session.refs[refKey]
if (refData) {
targetSelector = refData.selector
}
}
if (!targetSelector) throw new Error('Could not resolve selector')
await page.locator(targetSelector).click({ timeout: 10000 })
await page.keyboard.type(text, { delay: 50 })
if (submit) {
await page.keyboard.press('Enter')
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {})
}
const newSessionId = sessionId || randomUUID()
const newSession = await extractSessionData(context, page, newSessionId, undefined, session?.httpCredentials)
await saveSession(newSession)
const pageUrl = page.url()
await browser.close()
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
session_id: newSessionId,
url: pageUrl,
message: `タイプしました: ${text}${submit ? ' (送信済み)' : ''}`,
}, null, 2),
}],
}
}
case 'browser_get_text': {
const sessionId = args?.session_id as string | undefined
const url = args?.url as string | undefined
const ref = args?.ref as string | undefined
const selector = args?.selector as string | undefined
if (!sessionId && !url) throw new Error('session_id or url is required')
if (!ref && !selector) throw new Error('ref or selector is required')
let session = sessionId ? await getSession(sessionId) : null
const targetUrl = url || session?.url
if (!targetUrl) throw new Error('No URL available')
browser = await launchBrowser()
const { context, page } = await setupPage(browser, session, session?.httpCredentials)
await page.goto(targetUrl, { waitUntil: 'networkidle', timeout: 30000 })
let targetSelector = selector
if (ref && session?.refs) {
const refKey = ref.startsWith('@') ? ref.slice(1) : ref
const refData = session.refs[refKey]
if (refData) {
targetSelector = refData.selector
}
}
if (!targetSelector) throw new Error('Could not resolve selector')
const text = await page.locator(targetSelector).textContent({ timeout: 10000 })
const newSessionId = sessionId || randomUUID()
const newSession = await extractSessionData(context, page, newSessionId, undefined, session?.httpCredentials)
await saveSession(newSession)
await browser.close()
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
session_id: newSessionId,
text: text || '',
}, null, 2),
}],
}
}
case 'browser_screenshot': {
const sessionId = args?.session_id as string | undefined
const url = args?.url as string | undefined
const fullPage = args?.full_page as boolean
if (!sessionId && !url) throw new Error('session_id or url is required')
let session = sessionId ? await getSession(sessionId) : null
const targetUrl = url || session?.url
if (!targetUrl) throw new Error('No URL available')
browser = await launchBrowser()
const { context, page } = await setupPage(browser, session, session?.httpCredentials)
await page.goto(targetUrl, { waitUntil: 'networkidle', timeout: 30000 })
const screenshot = await page.screenshot({
fullPage: fullPage || false,
type: 'jpeg',
quality: 80,
})
const newSessionId = sessionId || randomUUID()
const newSession = await extractSessionData(context, page, newSessionId, undefined, session?.httpCredentials)
await saveSession(newSession)
const pageUrl = page.url()
await browser.close()
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
session_id: newSessionId,
url: pageUrl,
}, null, 2),
},
{
type: 'image',
data: screenshot.toString('base64'),
mimeType: 'image/jpeg',
},
],
}
}
case 'browser_wait': {
const sessionId = args?.session_id as string | undefined
const url = args?.url as string | undefined
const time = args?.time as number | undefined
const text = args?.text as string | undefined
const selector = args?.selector as string | undefined
if (!sessionId && !url) throw new Error('session_id or url is required')
if (!time && !text && !selector) throw new Error('time, text, or selector is required')
let session = sessionId ? await getSession(sessionId) : null
const targetUrl = url || session?.url
if (!targetUrl) throw new Error('No URL available')
browser = await launchBrowser()
const { context, page } = await setupPage(browser, session, session?.httpCredentials)
await page.goto(targetUrl, { waitUntil: 'networkidle', timeout: 30000 })
let waitResult = ''
if (time) {
await page.waitForTimeout(time * 1000)
waitResult = `${time}秒待機しました`
} else if (text) {
await page.waitForSelector(`text=${text}`, { timeout: 30000 })
waitResult = `テキスト "${text}" が出現しました`
} else if (selector) {
await page.waitForSelector(selector, { timeout: 30000 })
waitResult = `要素 "${selector}" が出現しました`
}
const newSessionId = sessionId || randomUUID()
const newSession = await extractSessionData(context, page, newSessionId, undefined, session?.httpCredentials)
await saveSession(newSession)
const pageUrl = page.url()
await browser.close()
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
session_id: newSessionId,
url: pageUrl,
message: waitResult,
}, null, 2),
}],
}
}
case 'browser_press_key': {
const sessionId = args?.session_id as string | undefined
const url = args?.url as string | undefined
const key = args?.key as string
if (!key) throw new Error('key is required')
if (!sessionId && !url) throw new Error('session_id or url is required')
let session = sessionId ? await getSession(sessionId) : null
const targetUrl = url || session?.url
if (!targetUrl) throw new Error('No URL available')
browser = await launchBrowser()
const { context, page } = await setupPage(browser, session, session?.httpCredentials)
await page.goto(targetUrl, { waitUntil: 'networkidle', timeout: 30000 })
await page.keyboard.press(key)
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
const newSessionId = sessionId || randomUUID()
const newSession = await extractSessionData(context, page, newSessionId, undefined, session?.httpCredentials)
await saveSession(newSession)
const pageUrl = page.url()
await browser.close()
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
session_id: newSessionId,
url: pageUrl,
message: `キー "${key}" を押しました`,
}, null, 2),
}],
}
}
case 'browser_close': {
const sessionId = args?.session_id as string
if (!sessionId) throw new Error('session_id is required')
await deleteSession(sessionId)
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
message: `セッション ${sessionId} を終了しました`,
}, null, 2),
}],
}
}
default:
throw new Error(`Unknown tool: ${name}`)
}
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error)
return {
content: [{ type: 'text', text: JSON.stringify({ error: true, message: errorMessage }, null, 2) }],
isError: true,
}
} finally {
if (browser) {
await browser.close().catch(() => {})
}
}
}
// Authentication helper
function authenticate(req: VercelRequest, res: VercelResponse): boolean {
if (requireAuth && authTokens.length === 0) {
res.status(500).json({
jsonrpc: '2.0',
error: { code: -32002, message: 'Server misconfigured: MCP_AUTH_TOKEN or AUTH_TOKEN is required' },
id: null
})
return false
}
if (authTokens.length === 0) return true
const authHeader = req.headers['authorization']
const authHeaderStr = Array.isArray(authHeader) ? authHeader[0] : authHeader
const bearerToken =
authHeaderStr && typeof authHeaderStr === 'string' && authHeaderStr.toLowerCase().startsWith('bearer ')
? authHeaderStr.slice('bearer '.length).trim()
: null
const directTokenHeader = req.headers['x-auth-token']
const directToken = Array.isArray(directTokenHeader) ? directTokenHeader[0] : directTokenHeader
const token = bearerToken || (typeof directToken === 'string' ? directToken : null)
if (!token || !authTokens.includes(token)) {
res.status(401).json({
jsonrpc: '2.0',
error: { code: -32001, message: 'Unauthorized' },
id: null
})
return false
}
return true
}
// JSON-RPC request handler for serverless environment
async function handleJsonRpcRequest(body: any, sessionId: string | undefined): Promise<{ result?: any; error?: any; newSessionId?: string }> {
const { method, params, id } = body
switch (method) {
case 'initialize':
const newSessionId = sessionId || randomUUID()
return {
result: {
protocolVersion: '2024-11-05',
capabilities: { tools: {} },
serverInfo: { name: 'agent-browser-mcp-server', version: '1.0.0' }
},
newSessionId
}
case 'notifications/initialized':
// Just acknowledge the notification
return {}
case 'tools/list':
return { result: { tools } }
case 'tools/call':
if (!params?.name) {
return { error: { code: -32602, message: 'Invalid params: missing tool name' } }
}
const toolResult = await executeTool(params.name, params.arguments || {})
return { result: toolResult }
case 'ping':
return { result: {} }
default:
return { error: { code: -32601, message: `Method not found: ${method}` } }
}
}
export default async function handler(req: VercelRequest, res: VercelResponse) {
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, mcp-session-id')
if (req.method === 'OPTIONS') {
res.status(200).end()
return
}
if (!authenticate(req, res)) return
try {
if (req.method === 'POST') {
const sessionId = req.headers['mcp-session-id'] as string | undefined
const body = req.body
const requestId = body?.id
// Handle JSON-RPC request directly (serverless-friendly approach)
const response = await handleJsonRpcRequest(body, sessionId)
// If this was an initialize request, set the session ID header
if (response.newSessionId) {
res.setHeader('mcp-session-id', response.newSessionId)
}
// For notifications (no id), don't send a response body
if (requestId === undefined || requestId === null) {
res.status(200).end()
return
}
// Send SSE-formatted response for compatibility
res.setHeader('Content-Type', 'text/event-stream')
res.setHeader('Cache-Control', 'no-cache')
if (response.error) {
const errorResponse = {
jsonrpc: '2.0',
error: response.error,
id: requestId
}
res.write(`event: message\ndata: ${JSON.stringify(errorResponse)}\n\n`)
} else if (response.result !== undefined) {
const successResponse = {
jsonrpc: '2.0',
result: response.result,
id: requestId
}
res.write(`event: message\ndata: ${JSON.stringify(successResponse)}\n\n`)
}
res.end()
} else if (req.method === 'GET') {
// SSE endpoint - not supported in serverless
res.status(400).json({
jsonrpc: '2.0',
error: { code: -32000, message: 'SSE not supported in serverless environment. Use POST requests.' },
id: null
})
} else if (req.method === 'DELETE') {
// Session cleanup - not needed in serverless
res.status(200).json({ success: true })
} else {
res.status(405).json({
jsonrpc: '2.0',
error: { code: -32601, message: 'Method not allowed' },
id: null
})
}
} catch (error) {
console.error('MCP Error:', error)
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: { code: -32603, message: `Internal error: ${error instanceof Error ? error.message : 'Unknown'}` },
id: null
})
}
}
}