diff --git a/.opencode/plugins/crawlith.ts b/.opencode/plugins/crawlith.ts new file mode 100644 index 0000000..c6dd668 --- /dev/null +++ b/.opencode/plugins/crawlith.ts @@ -0,0 +1,144 @@ +import type { Plugin } from '@opencode-ai/plugin'; +import { tool } from '@opencode-ai/plugin'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const CLI_PATH = path.resolve(__dirname, '../../packages/cli/dist/index.js'); + +/** + * Execute a Crawlith CLI command in JSON mode. + * + * @param $ OpenCode shell helper. + * @param commandArgs Command arguments excluding global JSON format flags. + * @returns Command output text. + */ +async function runCli( + $: { (strings: TemplateStringsArray, ...values: any[]): Promise<{ text(): Promise }> }, + commandArgs: string +): Promise { + const command = `node ${CLI_PATH} --format json ${commandArgs}`; + const result = await $`sh -c ${command}`; + return result.text(); +} + +/** + * Crawlith OpenCode plugin exposing Crawlith CLI tools and useful lifecycle hooks. + */ +export const CrawlithPlugin: Plugin = async ({ project, client, $, directory }) => { + await client.app.log({ + body: { + service: 'crawlith-plugin', + level: 'info', + message: `Initialized for project: ${project.name}` + } + }); + + return { + tool: { + 'crawlith-crawl-site': tool({ + description: 'Crawl an entire site and build Crawlith graph snapshot data.', + args: { + url: tool.schema.string().describe('Site URL or domain to crawl'), + limit: tool.schema.number().optional().describe('Maximum number of pages to crawl'), + depth: tool.schema.number().optional().describe('Maximum click depth'), + concurrency: tool.schema.number().optional().describe('Max concurrent requests') + }, + async execute({ url, limit, depth, concurrency }) { + const flags = [ + typeof limit === 'number' ? `--limit ${limit}` : '', + typeof depth === 'number' ? `--depth ${depth}` : '', + typeof concurrency === 'number' ? `--concurrency ${concurrency}` : '' + ] + .filter(Boolean) + .join(' '); + + return runCli($, `crawl ${url} ${flags}`.trim()); + } + }), + + 'crawlith-analyze-page': tool({ + description: 'Analyze a single page URL for SEO, content, and accessibility signals.', + args: { + url: tool.schema.string().describe('Page URL to analyze'), + live: tool.schema.boolean().optional().describe('Run with --live flag') + }, + async execute({ url, live }) { + const flags = live ? '--live' : ''; + return runCli($, `page ${url} ${flags}`.trim()); + } + }), + + 'crawlith-probe-domain': tool({ + description: 'Inspect TLS/transport and HTTP behavior for a domain or URL.', + args: { + url: tool.schema.string().describe('Domain or URL to inspect'), + timeout: tool.schema.number().optional().describe('Probe timeout in milliseconds') + }, + async execute({ url, timeout }) { + const flags = typeof timeout === 'number' ? `--timeout ${timeout}` : ''; + return runCli($, `probe ${url} ${flags}`.trim()); + } + }), + + 'crawlith-list-sites': tool({ + description: 'List all tracked sites from Crawlith local storage.', + args: {}, + async execute() { + return runCli($, 'sites'); + } + }) + }, + + event: async ({ event }) => { + if (event.type === 'session.idle') { + await client.app.log({ + body: { + service: 'crawlith-plugin', + level: 'info', + message: 'Session reached idle state.' + } + }); + } + }, + + 'tool.execute.before': async (input) => { + await client.app.log({ + body: { + service: 'crawlith-plugin', + level: 'debug', + message: `Tool called: ${input.tool}` + } + }); + }, + + 'tool.execute.after': async (input, output) => { + const crawlithTools = new Set([ + 'crawlith-crawl-site', + 'crawlith-analyze-page', + 'crawlith-probe-domain', + 'crawlith-list-sites' + ]); + + if (!crawlithTools.has(input.tool)) { + return; + } + + const reportsDir = path.join(directory, 'crawlith-opencode-reports'); + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const reportPath = path.join(reportsDir, `${input.tool}-${timestamp}.json`); + + await $`mkdir -p ${reportsDir}`; + await $`sh -c ${`cat > ${reportPath} <<'JSON'\n${JSON.stringify(output.output, null, 2)}\nJSON`}`; + + await client.app.log({ + body: { + service: 'crawlith-plugin', + level: 'info', + message: `Saved tool output: ${reportPath}` + } + }); + } + }; +}; diff --git a/opencode.json b/opencode.json new file mode 100644 index 0000000..cd8e427 --- /dev/null +++ b/opencode.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "crawlith": { + "type": "local", + "command": "npx", + "args": ["tsx", "./packages/mcp/src/mcp-server.ts"] + } + } +} diff --git a/packages/core/src/plugin-system/plugin-types.ts b/packages/core/src/plugin-system/plugin-types.ts index eb2d9a0..ea39d3b 100644 --- a/packages/core/src/plugin-system/plugin-types.ts +++ b/packages/core/src/plugin-system/plugin-types.ts @@ -67,9 +67,45 @@ export interface PluginContext { /** Legacy logger alias — kept for backwards compatibility. */ logger?: CLIWriter; metadata?: Record; + /** MCP discovery registry available during MCP startup discovery. */ + mcpDiscovery?: PluginMcpDiscovery; [key: string]: any; } +// ─── MCP Discovery ─────────────────────────────────────────────────────────── + +export interface PluginMcpTool { + name: string; + description: string; + /** Zod-like shape object interpreted by the MCP server package. */ + inputSchema?: Record; + execute: (input: Record, ctx: PluginContext) => unknown | Promise; +} + +export interface PluginMcpPrompt { + name: string; + description: string; + /** Zod-like shape object interpreted by the MCP server package. */ + argumentsSchema?: Record; + buildMessages: ( + input: Record, + ctx: PluginContext + ) => { + messages: Array<{ + role: 'user' | 'assistant' | 'system'; + content: { + type: 'text'; + text: string; + }; + }>; + }; +} + +export interface PluginMcpDiscovery { + registerTool(tool: PluginMcpTool): void; + registerPrompt(prompt: PluginMcpPrompt): void; +} + // ─── CLI option declaration ─────────────────────────────────────────────────── export interface PluginCliOption { @@ -141,6 +177,12 @@ export interface CrawlithPlugin { */ register?: (cli: any) => void; + /** Declarative MCP definitions discovered by @crawlith/mcp at startup. */ + mcp?: { + tools?: PluginMcpTool[]; + prompts?: PluginMcpPrompt[]; + }; + hooks?: { /** * Runs on both `page` and `crawl` scopes. @@ -193,5 +235,11 @@ export interface CrawlithPlugin { * Attach snapshot-level summary data to the result object. */ onReport?: (ctx: PluginContext, report: any) => void | Promise; + + /** + * MCP discovery phase — executed by @crawlith/mcp server startup. + * Plugins can register MCP tools/prompts through `ctx.mcpDiscovery`. + */ + onMcpDiscovery?: (ctx: PluginContext) => void | Promise; }; } diff --git a/packages/mcp/README.md b/packages/mcp/README.md new file mode 100644 index 0000000..299bd95 --- /dev/null +++ b/packages/mcp/README.md @@ -0,0 +1,56 @@ +# @crawlith/mcp + +MCP server that exposes Crawlith CLI workflows as MCP tools for Claude Desktop. + +## Tools + +- `ensure_crawlith_cli` — checks whether Crawlith CLI is available and can install `@crawlith/cli` if missing +- `crawl_site` — wraps `crawlith crawl` +- `analyze_page` — wraps `crawlith page` +- `probe_domain` — wraps `crawlith probe` +- `list_sites` — wraps `crawlith sites` + +## Prompts + +- `full_site_audit` +- `portfolio_status` + +## Plugin MCP Discovery + +`@crawlith/mcp` now discovers plugin-provided MCP tools and prompts at startup: + +- Declarative: plugins can expose `plugin.mcp.tools` and `plugin.mcp.prompts` +- Hook-based: plugins can implement `hooks.onMcpDiscovery(ctx)` and register through `ctx.mcpDiscovery.registerTool(...)` / `ctx.mcpDiscovery.registerPrompt(...)` + +## Package dependency + +This package depends on `@crawlith/cli`, so when `@crawlith/mcp` is published and installed from npm, it can resolve the Crawlith CLI entrypoint from `node_modules`. + +## Run + +```bash +pnpm --filter @crawlith/mcp run mcp +``` + +By default, the server resolves and executes the installed `@crawlith/cli` entrypoint. +When developing inside this monorepo, it falls back to `packages/cli/dist/index.js`. + +Override either behavior with `CRAWLITH_CLI_COMMAND` if you need a custom CLI command. + +## Claude Desktop configuration + +Add the server to `claude_desktop_config.json` with an absolute path: + +```json +{ + "mcpServers": { + "crawlith": { + "command": "npx", + "args": [ + "tsx", + "/absolute/path/to/crawlith/packages/mcp/src/mcp-server.ts" + ] + } + } +} +``` diff --git a/packages/mcp/package.json b/packages/mcp/package.json new file mode 100644 index 0000000..5bfc7f6 --- /dev/null +++ b/packages/mcp/package.json @@ -0,0 +1,20 @@ +{ + "name": "@crawlith/mcp", + "license": "Apache-2.0", + "version": "0.1.0", + "type": "module", + "description": "MCP server bridge for Crawlith CLI tools.", + "scripts": { + "mcp": "tsx src/mcp-server.ts", + "test:mcp": "tsx src/mcp-server.ts --help" + }, + "dependencies": { + "@crawlith/cli": "workspace:*", + "@modelcontextprotocol/sdk": "^1.17.5", + "zod": "^3.25.76" + }, + "devDependencies": { + "tsx": "^4.20.5", + "typescript": "^5.9.2" + } +} diff --git a/packages/mcp/src/mcp-server.ts b/packages/mcp/src/mcp-server.ts new file mode 100644 index 0000000..5b23f5a --- /dev/null +++ b/packages/mcp/src/mcp-server.ts @@ -0,0 +1,447 @@ +import { execFile } from 'node:child_process'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createRequire } from 'node:module'; +import { promisify } from 'node:util'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { z } from 'zod'; +import { + PluginLoader, + type CrawlithPlugin, + type PluginContext, + type PluginMcpPrompt, + type PluginMcpTool +} from '@crawlith/core'; +import { registerPrompts } from './prompts.js'; + +const execFileAsync = promisify(execFile); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const workspaceRoot = path.resolve(__dirname, '../../..'); +const require = createRequire(import.meta.url); + +/** + * Resolve the Crawlith CLI entrypoint from dependency install first, + * then fall back to the local monorepo build path for development. + * + * @returns Absolute path to CLI JavaScript entrypoint. + */ +function resolveDefaultCliEntrypoint(): string { + try { + return require.resolve('@crawlith/cli/dist/index.js'); + } catch { + return path.resolve(workspaceRoot, 'packages/cli/dist/index.js'); + } +} + +const defaultCliEntrypoint = resolveDefaultCliEntrypoint(); + +/** + * Resolve the CLI invocation used by MCP tools. + * + * The command can be overridden by setting `CRAWLITH_CLI_COMMAND` to a + * shell-like string, for example: `node /absolute/path/to/index.js`. + * + * @returns Executable file and static arguments. + */ +function resolveCliCommand(): { file: string; args: string[] } { + const configured = process.env.CRAWLITH_CLI_COMMAND?.trim(); + if (!configured) { + return { + file: process.execPath, + args: [defaultCliEntrypoint] + }; + } + + const tokens = configured.split(/\s+/).filter(Boolean); + const [file, ...args] = tokens; + return { file, args }; +} + +/** + * Build a CLI argument list by prepending global JSON format output. + * + * @param commandArgs Command-specific arguments. + * @returns Combined argument list for `crawlith` invocation. + */ +function withJsonOutput(commandArgs: string[]): string[] { + return ['--format', 'json', ...commandArgs]; +} + +/** + * Run the Crawlith CLI and parse its output. + * + * @param commandArgs Command arguments (without global format flags). + * @returns Parsed JSON payload or raw stdout when JSON parsing fails. + */ +async function runCliCommand(commandArgs: string[]): Promise { + const cli = resolveCliCommand(); + const args = [...cli.args, ...withJsonOutput(commandArgs)]; + + const { stdout, stderr } = await execFileAsync(cli.file, args, { + cwd: workspaceRoot, + env: process.env, + maxBuffer: 10 * 1024 * 1024 + }); + + if (stderr?.trim()) { + throw new Error(stderr.trim()); + } + + const trimmed = stdout.trim(); + if (!trimmed) { + return ''; + } + + try { + return JSON.parse(trimmed); + } catch { + return trimmed; + } +} + +/** + * Convert unknown CLI results into MCP text content. + * + * @param payload Result returned from `runCliCommand`. + * @returns MCP-compatible content envelope. + */ +function asTextContent(payload: unknown): { content: Array<{ type: 'text'; text: string }> } { + const text = typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2); + return { + content: [{ type: 'text', text }] + }; +} + + +/** + * Execute a command and return stdout/stderr as utf8 text. + * + * @param file Executable name or path. + * @param args Argument list. + * @returns Process output object. + */ +async function runProcess(file: string, args: string[]): Promise<{ stdout: string; stderr: string }> { + return execFileAsync(file, args, { + cwd: workspaceRoot, + env: process.env, + maxBuffer: 10 * 1024 * 1024 + }); +} + +/** + * Verify Crawlith CLI availability and optionally install it if absent. + * + * @param installWhenMissing Whether to install `@crawlith/cli` if version check fails. + * @param packageManager Package manager to execute installation with. + * @returns Installation/check report. + */ +async function ensureCliAvailable( + installWhenMissing: boolean, + packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun' +): Promise<{ installed: boolean; checkedWith: string; installAttempted: boolean; installOutput?: string }> { + const cli = resolveCliCommand(); + try { + await runProcess(cli.file, [...cli.args, '--version']); + return { + installed: true, + checkedWith: `${cli.file} ${cli.args.join(' ')}`.trim(), + installAttempted: false + }; + } catch (initialError) { + if (!installWhenMissing) { + return { + installed: false, + checkedWith: `${cli.file} ${cli.args.join(' ')}`.trim(), + installAttempted: false, + installOutput: String(initialError) + }; + } + + const installArgsByManager: Record<'npm' | 'pnpm' | 'yarn' | 'bun', string[]> = { + npm: ['install', '@crawlith/cli', '--no-save'], + pnpm: ['add', '@crawlith/cli', '--no-save'], + yarn: ['add', '@crawlith/cli', '--no-lockfile'], + bun: ['add', '@crawlith/cli'] + }; + + const installArgs = installArgsByManager[packageManager]; + const installResult = await runProcess(packageManager, installArgs); + + await runProcess(cli.file, [...cli.args, '--version']); + return { + installed: true, + checkedWith: `${cli.file} ${cli.args.join(' ')}`.trim(), + installAttempted: true, + installOutput: [installResult.stdout, installResult.stderr].filter(Boolean).join('\n').trim() + }; + } +} + +/** + * Discover plugin-provided MCP tools and prompts. + * + * @returns Plugin MCP definitions merged from declarative and hook-based discovery. + */ +async function discoverPluginMcpDefinitions(): Promise<{ + tools: PluginMcpTool[]; + prompts: PluginMcpPrompt[]; +}> { + const loader = new PluginLoader(); + const plugins = await loader.discover(workspaceRoot); + const tools: PluginMcpTool[] = []; + const prompts: PluginMcpPrompt[] = []; + + for (const plugin of plugins) { + if (plugin.mcp?.tools?.length) tools.push(...plugin.mcp.tools); + if (plugin.mcp?.prompts?.length) prompts.push(...plugin.mcp.prompts); + + if (plugin.hooks?.onMcpDiscovery) { + const context: PluginContext = createPluginDiscoveryContext(plugin, tools, prompts); + await plugin.hooks.onMcpDiscovery(context); + } + } + + return { tools, prompts }; +} + +/** + * Build a PluginContext object for MCP discovery hooks. + * + * @param plugin Plugin currently being discovered. + * @param tools Aggregate tool registry. + * @param prompts Aggregate prompt registry. + * @returns PluginContext for `onMcpDiscovery`. + */ +function createPluginDiscoveryContext( + plugin: CrawlithPlugin, + tools: PluginMcpTool[], + prompts: PluginMcpPrompt[] +): PluginContext { + return { + scope: 'crawl', + command: 'mcp-discovery', + metadata: { plugin: plugin.name }, + mcpDiscovery: { + registerTool(tool: PluginMcpTool): void { + tools.push(tool); + }, + registerPrompt(prompt: PluginMcpPrompt): void { + prompts.push(prompt); + } + }, + logger: { + info(message: string): void { + console.error(`[plugin:${plugin.name}] ${message}`); + }, + warn(message: string): void { + console.error(`[plugin:${plugin.name}] ${message}`); + }, + error(message: string): void { + console.error(`[plugin:${plugin.name}] ${message}`); + }, + debug(message: string): void { + console.error(`[plugin:${plugin.name}] ${message}`); + } + } + }; +} + +/** + * Register plugin-discovered MCP definitions on the server. + * + * @param mcpServer Server to register with. + * @returns Number of registered tools and prompts. + */ +async function registerPluginMcpDefinitions(mcpServer: McpServer): Promise<{ tools: number; prompts: number }> { + const { tools, prompts } = await discoverPluginMcpDefinitions(); + const usedNames = new Set([ + 'ensure_crawlith_cli', + 'crawl_site', + 'analyze_page', + 'probe_domain', + 'list_sites', + 'full_site_audit', + 'portfolio_status' + ]); + + let toolCount = 0; + let promptCount = 0; + + for (const tool of tools) { + if (!tool?.name || usedNames.has(tool.name)) { + continue; + } + usedNames.add(tool.name); + mcpServer.tool( + tool.name, + tool.description, + (tool.inputSchema ?? {}) as Record, + async (input: Record) => { + const result = await tool.execute(input, { + scope: 'crawl', + command: 'mcp-tool', + metadata: { source: 'plugin', tool: tool.name } + }); + return asTextContent(result); + } + ); + toolCount += 1; + } + + for (const prompt of prompts) { + if (!prompt?.name || usedNames.has(prompt.name)) { + continue; + } + usedNames.add(prompt.name); + mcpServer.prompt( + prompt.name, + prompt.description, + (prompt.argumentsSchema ?? {}) as Record, + (input: Record) => prompt.buildMessages(input, { + scope: 'crawl', + command: 'mcp-prompt', + metadata: { source: 'plugin', prompt: prompt.name } + }) + ); + promptCount += 1; + } + + return { tools: toolCount, prompts: promptCount }; +} + +const server = new McpServer({ + name: 'crawlith-mcp', + version: '0.1.0' +}); + +server.tool( + 'ensure_crawlith_cli', + 'Check whether Crawlith CLI is available and optionally install @crawlith/cli when missing.', + { + installWhenMissing: z.boolean().default(true).describe('Install @crawlith/cli when the CLI is missing.'), + packageManager: z.enum(['npm', 'pnpm', 'yarn', 'bun']).default('npm').describe('Package manager used for installation.') + }, + async ({ + installWhenMissing, + packageManager + }: { + installWhenMissing: boolean; + packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun'; + }) => { + const result = await ensureCliAvailable(installWhenMissing, packageManager); + return asTextContent(result); + } +); + +server.tool( + 'crawl_site', + 'Run the Crawlith crawl command to build a site graph snapshot.', + { + url: z.string().describe('Site URL or domain to crawl.'), + limit: z.number().int().positive().max(50000).optional().describe('Maximum page count.'), + depth: z.number().int().positive().max(100).optional().describe('Maximum click depth.'), + concurrency: z.number().int().positive().max(20).optional().describe('Maximum parallel requests.'), + noQuery: z.boolean().optional().describe('Strip query parameters while crawling.'), + sitemap: z.string().optional().describe('Optional sitemap URL to seed crawling.') + }, + async ({ + url, + limit, + depth, + concurrency, + noQuery, + sitemap + }: { + url: string; + limit?: number; + depth?: number; + concurrency?: number; + noQuery?: boolean; + sitemap?: string; + }) => { + const args = ['crawl', url]; + if (typeof limit === 'number') args.push('--limit', String(limit)); + if (typeof depth === 'number') args.push('--depth', String(depth)); + if (typeof concurrency === 'number') args.push('--concurrency', String(concurrency)); + if (noQuery) args.push('--no-query'); + if (sitemap) args.push('--sitemap', sitemap); + + const result = await runCliCommand(args); + return asTextContent(result); + } +); + +server.tool( + 'analyze_page', + 'Analyze one URL for SEO, content, and accessibility signals using Crawlith page command.', + { + url: z.string().describe('Page URL to analyze.'), + live: z.boolean().optional().describe('Perform a live crawl before analysis.'), + module: z.enum(['all', 'seo', 'content', 'accessibility']).optional().describe('Optional module filter.') + }, + async ({ url, live, module }: { + url: string; + live?: boolean; + module?: 'all' | 'seo' | 'content' | 'accessibility'; + }) => { + const args = ['page', url]; + if (live) args.push('--live'); + if (module === 'seo') args.push('--seo'); + if (module === 'content') args.push('--content'); + if (module === 'accessibility') args.push('--accessibility'); + + const result = await runCliCommand(args); + return asTextContent(result); + } +); + +server.tool( + 'probe_domain', + 'Inspect transport, TLS, and HTTP behavior using Crawlith probe command.', + { + url: z.string().describe('Domain or URL to inspect.'), + timeoutMs: z.number().int().positive().max(120000).optional().describe('Probe request timeout in milliseconds.') + }, + async ({ url, timeoutMs }: { url: string; timeoutMs?: number }) => { + const args = ['probe', url]; + if (typeof timeoutMs === 'number') args.push('--timeout', String(timeoutMs)); + + const result = await runCliCommand(args); + return asTextContent(result); + } +); + +server.tool( + 'list_sites', + 'Return all tracked sites and latest snapshot summaries from Crawlith local database.', + {}, + async () => { + const result = await runCliCommand(['sites']); + return asTextContent(result); + } +); + +registerPrompts(server); + +/** + * Connect the MCP server over stdio transport. + * + * @returns Promise that resolves when transport is connected. + */ +async function main(): Promise { + const discovered = await registerPluginMcpDefinitions(server); + const transport = new StdioServerTransport(); + await server.connect(transport); + if (discovered.tools > 0 || discovered.prompts > 0) { + console.error(`Plugin MCP discovery: ${discovered.tools} tools, ${discovered.prompts} prompts.`); + } + console.error('Crawlith MCP server running...'); +} + +main().catch((error) => { + console.error('Fatal error:', error); + process.exit(1); +}); diff --git a/packages/mcp/src/prompts.ts b/packages/mcp/src/prompts.ts new file mode 100644 index 0000000..51d38fa --- /dev/null +++ b/packages/mcp/src/prompts.ts @@ -0,0 +1,63 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { z } from 'zod'; + +/** + * Register Crawlith MCP prompts on the provided server instance. + * + * @param server MCP server that owns prompt registrations. + * @returns Nothing. + */ +export function registerPrompts(server: McpServer): void { + server.prompt( + 'full_site_audit', + 'Perform a complete Crawlith workflow for one site and summarize key findings.', + { + url: z.string().describe('Site URL or domain to audit.') + }, + ({ url }: { url: string }) => ({ + messages: [ + { + role: 'user', + content: { + type: 'text', + text: ` +Run a full Crawlith audit for ${url}. + +Steps: +1. Use crawl_site to create or refresh a crawl snapshot. +2. Use analyze_page on the homepage URL and identify SEO/content/accessibility concerns. +3. Use probe_domain to evaluate transport and TLS posture. +4. Summarize critical issues, health indicators, and prioritized next actions. + +Be explicit about values returned by the tools. + `.trim() + } + } + ] + }) + ); + + server.prompt( + 'portfolio_status', + 'Review all tracked Crawlith sites and identify where to investigate first.', + {}, + () => ({ + messages: [ + { + role: 'user', + content: { + type: 'text', + text: ` +Assess Crawlith portfolio status. + +Steps: +1. Use list_sites to fetch all tracked sites. +2. Rank sites by crawl recency, page coverage, and health. +3. Recommend the top three sites that need immediate auditing and why. + `.trim() + } + } + ] + }) + ); +} diff --git a/packages/mcp/tsconfig.json b/packages/mcp/tsconfig.json new file mode 100644 index 0000000..1487dab --- /dev/null +++ b/packages/mcp/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "declaration": true + }, + "include": ["src"] +}