@lattis-dev/cli
Advanced tools
+137
| #!/usr/bin/env node | ||
| /** | ||
| * Lattis MCP Server | ||
| * | ||
| * Exposes Lattis data to AI agents via Model Context Protocol. | ||
| * Works with Claude Code, Cursor, and any MCP client. | ||
| * | ||
| * Usage: | ||
| * npx @lattis-dev/cli mcp (via CLI) | ||
| * lattis-mcp (standalone binary) | ||
| * | ||
| * Claude Code config (~/.claude/settings.json): | ||
| * { | ||
| * "mcpServers": { | ||
| * "lattis": { | ||
| * "command": "npx", | ||
| * "args": ["-y", "@lattis-dev/cli", "mcp"] | ||
| * } | ||
| * } | ||
| * } | ||
| */ | ||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; | ||
| import { z } from "zod"; | ||
| const API = process.env.LATTIS_API_URL ?? "https://api.lattis.dev"; | ||
| async function api(path, options) { | ||
| let resp; | ||
| try { | ||
| resp = await fetch(`${API}${path}`, { | ||
| headers: { "Content-Type": "application/json" }, | ||
| ...options, | ||
| }); | ||
| } | ||
| catch (err) { | ||
| throw new Error(`API unreachable: ${err instanceof Error ? err.message : String(err)}`); | ||
| } | ||
| if (!resp.ok) { | ||
| const text = await resp.text().catch(() => ""); | ||
| throw new Error(`API ${path} returned ${resp.status}: ${text}`); | ||
| } | ||
| return resp.json(); | ||
| } | ||
| function errorResult(message) { | ||
| return { content: [{ type: "text", text: message }], isError: true }; | ||
| } | ||
| const server = new McpServer({ | ||
| name: "lattis", | ||
| version: "0.2.0", | ||
| }); | ||
| server.tool("lattis_list_sites", "List all websites indexed by Lattis. Returns site name, category, item count, and URL for each indexed site.", {}, async () => { | ||
| try { | ||
| const data = await api("/sites"); | ||
| const sites = data.sites.map((s) => ({ | ||
| domain: s.domain, | ||
| name: s.analysis?.name ?? s.domain, | ||
| category: s.analysis?.category ?? "unknown", | ||
| items: s.item_count, | ||
| url: s.url, | ||
| status: s.status, | ||
| })); | ||
| return { content: [{ type: "text", text: JSON.stringify(sites, null, 2) }] }; | ||
| } | ||
| catch (err) { | ||
| return errorResult(`Failed to list sites: ${err instanceof Error ? err.message : String(err)}`); | ||
| } | ||
| }); | ||
| server.tool("lattis_search", "Search across all indexed websites for facts, capabilities, and actions. Returns ranked results with source URLs. Use this to find real data about products, services, pricing, and how to accomplish tasks.", { | ||
| query: z.string().describe("Search query, e.g. 'analytics for startups' or 'book a meeting'"), | ||
| site: z.string().optional().describe("Optional: limit search to a specific domain"), | ||
| type: z.string().optional().describe("Optional: filter by 'fact', 'capability', or 'action'"), | ||
| }, async ({ query, site, type }) => { | ||
| try { | ||
| const body = { q: query }; | ||
| if (site) | ||
| body.site = site; | ||
| if (type) | ||
| body.type = type; | ||
| const data = await api("/search", { method: "POST", body: JSON.stringify(body) }); | ||
| if (data.results.length === 0) { | ||
| return { content: [{ type: "text", text: `No results for "${query}"` }] }; | ||
| } | ||
| const output = data.results.map((r) => ({ | ||
| type: r.type, | ||
| text: r.text, | ||
| site: r.site_name, | ||
| domain: r.domain, | ||
| url: r.url, | ||
| })); | ||
| return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }] }; | ||
| } | ||
| catch (err) { | ||
| return errorResult(`Search failed: ${err instanceof Error ? err.message : String(err)}`); | ||
| } | ||
| }); | ||
| server.tool("lattis_get_items", "Get all extracted items (facts, capabilities, actions) for a specific website. Use to understand what a site offers, its pricing, features, and how to use it.", { | ||
| domain: z.string().describe("Domain, e.g. 'posthog.com' or 'cal.com'"), | ||
| type: z.string().optional().describe("Optional: filter by 'fact', 'capability', or 'action'"), | ||
| }, async ({ domain, type }) => { | ||
| try { | ||
| const typeParam = type ? `?type=${type}` : ""; | ||
| const data = await api(`/sites/${domain}/items${typeParam}`); | ||
| if (data.items.length === 0) { | ||
| return { content: [{ type: "text", text: `No items for "${domain}". Use lattis_list_sites to see available sites.` }] }; | ||
| } | ||
| return { content: [{ type: "text", text: JSON.stringify(data.items, null, 2) }] }; | ||
| } | ||
| catch (err) { | ||
| return errorResult(`Failed to get items: ${err instanceof Error ? err.message : String(err)}`); | ||
| } | ||
| }); | ||
| server.tool("lattis_get_page", "Get the full markdown content of a specific page from an indexed website.", { | ||
| domain: z.string().describe("Domain, e.g. 'posthog.com'"), | ||
| path: z.string().describe("Page path, e.g. '/pricing' or '/about'"), | ||
| }, async ({ domain, path }) => { | ||
| try { | ||
| const data = await api(`/sites/${domain}/pages${path}`); | ||
| if (data.error) { | ||
| const available = data.available?.map((p) => p.path).join(", ") ?? "none"; | ||
| return { content: [{ type: "text", text: `Page not found: ${path}. Available: ${available}` }] }; | ||
| } | ||
| const page = data.page; | ||
| return { content: [{ type: "text", text: `# ${page.title}\nURL: ${page.url}\n\n${page.markdown}` }] }; | ||
| } | ||
| catch (err) { | ||
| return errorResult(`Failed to get page: ${err instanceof Error ? err.message : String(err)}`); | ||
| } | ||
| }); | ||
| async function main() { | ||
| const transport = new StdioServerTransport(); | ||
| await server.connect(transport); | ||
| console.error("Lattis MCP server running on stdio"); | ||
| console.error(`API: ${API}`); | ||
| } | ||
| main().catch((err) => { | ||
| console.error("Fatal:", err); | ||
| process.exit(1); | ||
| }); |
+5
-1
| #!/usr/bin/env node | ||
| #!/usr/bin/env node | ||
| import { api } from "./api.js"; | ||
@@ -138,2 +137,3 @@ let agentMode = false; | ||
| lattis analyze <url> Submit a site for indexing | ||
| lattis mcp Start MCP server (for Claude Code, Cursor) | ||
@@ -186,2 +186,6 @@ Flags: | ||
| break; | ||
| case "mcp": | ||
| // Import and run MCP server | ||
| await import("./mcp.js"); | ||
| break; | ||
| default: | ||
@@ -188,0 +192,0 @@ // Treat unknown command as search |
+8
-3
| { | ||
| "name": "@lattis-dev/cli", | ||
| "version": "0.1.1", | ||
| "version": "0.2.0", | ||
| "repository": { | ||
@@ -12,3 +12,4 @@ "type": "git", | ||
| "bin": { | ||
| "lattis": "./dist/cli.js" | ||
| "lattis": "./dist/cli.js", | ||
| "lattis-mcp": "./dist/mcp.js" | ||
| }, | ||
@@ -20,3 +21,3 @@ "files": [ | ||
| "scripts": { | ||
| "build": "tsc && node -e \"const fs=require('fs');const f='dist/cli.js';fs.writeFileSync(f,'#!/usr/bin/env node\\n'+fs.readFileSync(f,'utf8'));fs.chmodSync(f,0o755)\"", | ||
| "build": "tsc && node -e \"const fs=require('fs');['dist/cli.js','dist/mcp.js'].forEach(f=>{if(!fs.existsSync(f))return;const c=fs.readFileSync(f,'utf8');if(!c.startsWith('#!'))fs.writeFileSync(f,'#!/usr/bin/env node\\n'+c);fs.chmodSync(f,0o755)})\"", | ||
| "dev": "tsx src/cli.ts", | ||
@@ -35,2 +36,6 @@ "prepublishOnly": "npm run build" | ||
| "license": "MIT", | ||
| "dependencies": { | ||
| "@modelcontextprotocol/sdk": "^1.12.1", | ||
| "zod": "^3.23.0" | ||
| }, | ||
| "devDependencies": { | ||
@@ -37,0 +42,0 @@ "@types/node": "^22", |
+41
-1
@@ -25,4 +25,44 @@ # @lattis-dev/cli | ||
| ## Usage | ||
| ## Use with Claude Code / Cursor (MCP) | ||
| Add to your Claude Code settings (`~/.claude/settings.json`): | ||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "lattis": { | ||
| "command": "npx", | ||
| "args": ["-y", "@lattis-dev/cli", "mcp"] | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| Or for Cursor, add to `.cursor/mcp.json`: | ||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "lattis": { | ||
| "command": "npx", | ||
| "args": ["-y", "@lattis-dev/cli", "mcp"] | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| This gives your AI 4 tools: `lattis_search`, `lattis_list_sites`, `lattis_get_items`, `lattis_get_page`. | ||
| ## Use with any AI (pipe mode) | ||
| ```bash | ||
| # Pipe structured JSON into your agent | ||
| lattis search "PostHog pricing" --agent | your-agent | ||
| # Or use as context in a prompt | ||
| echo "Based on this data: $(lattis site posthog.com --agent)" | llm | ||
| ``` | ||
| ## CLI Commands | ||
| ### Search across all indexed sites | ||
@@ -29,0 +69,0 @@ |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
17090
65.55%5
25%343
69.8%135
42.11%2
Infinity%3
50%2
100%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added