diff --git a/pubky-mcp-server/.gitignore b/pubky-mcp-server/.gitignore new file mode 100644 index 0000000..e3912be --- /dev/null +++ b/pubky-mcp-server/.gitignore @@ -0,0 +1,51 @@ +# MCP Server - Build output +dist/ +data/ + +# Dependencies +node_modules/ + +# NPM +*.tgz +package-lock.json + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# IDE +.vscode/ +.idea/ +.cursor/ +*.swp +*.swo +*~ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Environment +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Temporary files +tmp/ +temp/ +*.tmp + +# Build artifacts (general) +build/ +out/ + diff --git a/pubky-mcp-server/.npmignore b/pubky-mcp-server/.npmignore new file mode 100644 index 0000000..8e70cfd --- /dev/null +++ b/pubky-mcp-server/.npmignore @@ -0,0 +1,29 @@ +# Source files +src/ +tsconfig.json + +# Development files +node_modules/ +.cursor/ +scripts/ + +# Git +.git/ +.gitignore +**/.git/ + +# Test files +*.test.js +*.test.ts + +# Documentation (keep README.md and SETUP.md) +cursor-config-example.json + +# IDE +.vscode/ +.idea/ + +# Logs +*.log +npm-debug.log* + diff --git a/pubky-mcp-server/.oxlintignore b/pubky-mcp-server/.oxlintignore new file mode 100644 index 0000000..2789788 --- /dev/null +++ b/pubky-mcp-server/.oxlintignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +data/ +**/*.d.ts diff --git a/pubky-mcp-server/.oxlintrc.json b/pubky-mcp-server/.oxlintrc.json new file mode 100644 index 0000000..ffeb64e --- /dev/null +++ b/pubky-mcp-server/.oxlintrc.json @@ -0,0 +1,3 @@ +{ + "ignorePatterns": ["node_modules/", "dist/", "**/*.d.ts"] +} diff --git a/pubky-mcp-server/.prettierignore b/pubky-mcp-server/.prettierignore new file mode 100644 index 0000000..fb5d468 --- /dev/null +++ b/pubky-mcp-server/.prettierignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +data/ +*.d.ts +*.d.ts.map +*.js.map +package-lock.json diff --git a/pubky-mcp-server/.prettierrc b/pubky-mcp-server/.prettierrc new file mode 100644 index 0000000..bfa14e5 --- /dev/null +++ b/pubky-mcp-server/.prettierrc @@ -0,0 +1,12 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "bracketSpacing": true, + "arrowParens": "avoid", + "endOfLine": "lf", + "quoteProps": "as-needed" +} diff --git a/pubky-mcp-server/AGENTS.MD b/pubky-mcp-server/AGENTS.MD new file mode 100644 index 0000000..4ca0486 --- /dev/null +++ b/pubky-mcp-server/AGENTS.MD @@ -0,0 +1,383 @@ +# Instructions for AI Agents: Working with Pubky + +## Priority Order for Information Retrieval + +When working with Pubky-related questions, tasks, or development work, **ALWAYS follow this priority order**: + +### 1. **FIRST: Check Pubky MCP Server** ⭐ +### 2. **ONLY THEN: Search the Internet** 🌐 + +--- + +## About the Pubky MCP Server + +This project has a **comprehensive Model Context Protocol (MCP) server** that provides: +- βœ… Complete documentation for all 6 official Pubky projects +- βœ… 65+ development tools for code generation, validation, and project setup +- βœ… Working code examples in Rust and JavaScript/TypeScript +- βœ… Real-time testnet management +- βœ… API specifications for Nexus (social indexer) +- βœ… Data model specs and validation (pubky-app-specs) +- βœ… Pkarr (discovery layer) documentation and tools +- βœ… Pkdns (DNS resolver) guides + +**The MCP server is your single source of truth for Pubky.** + +--- + +## When to Use MCP Tools (ALWAYS FIRST) + +Use the MCP server for **ANY** Pubky-related question or task: + +### πŸ“š Understanding Pubky Concepts +- βœ… **USE MCP**: `get_pubky_concept` - Explains homeserver, rootkey, capabilities, auth, storage +- βœ… **USE MCP**: `search_documentation` - Searches all Pubky docs, examples, and code +- βœ… **USE MCP**: `get_pkarr_concept` - Explains Pkarr (DHT, relays, DNS records, keypairs) +- βœ… **USE MCP**: `get_pkdns_info` - DNS resolver information +- βœ… **USE MCP**: `get_nexus_architecture` - Indexer architecture and components + +### πŸ’» Code Examples & Generation +- βœ… **USE MCP**: `get_code_example` - Get auth, storage, signup examples +- βœ… **USE MCP**: `generate_app_scaffold` - Create new Pubky apps +- βœ… **USE MCP**: `get_code_template` - Pre-built code patterns +- βœ… **USE MCP**: `list_templates` - See all available templates +- βœ… **USE MCP**: `get_pkarr_example` - Pkarr publish/resolve examples +- βœ… **USE MCP**: `generate_pkarr_client` - Generate Pkarr client code +- βœ… **USE MCP**: `generate_pkarr_keypair` - Keypair management code +- βœ… **USE MCP**: `generate_dns_record_builder` - DNS record builder code + +### πŸ” Nexus API (Social Data Reading) +- βœ… **USE MCP**: `query_nexus_api` - Search Nexus endpoints (feeds, users, posts, search) +- βœ… **USE MCP**: `explain_nexus_endpoint` - Get endpoint details with examples +- βœ… **USE MCP**: `generate_nexus_client` - Auto-generate API clients + +### πŸ“ Data Models & Validation (Writing Social Data) +- βœ… **USE MCP**: `generate_data_model` - Generate User, Post, Tag, Bookmark models +- βœ… **USE MCP**: `validate_model_data` - Validate data against specs +- βœ… **USE MCP**: `explain_model` - Model schema and rules +- βœ… **USE MCP**: `create_model_example` - Working code examples + +### πŸ› οΈ Project Setup & Integration +- βœ… **USE MCP**: `analyze_project` - Analyze existing project structure +- βœ… **USE MCP**: `detect_environment` - Check installed tools (Node, Rust, Cargo) +- βœ… **USE MCP**: `suggest_setup` - Get setup recommendations +- βœ… **USE MCP**: `integrate_pubky` - Add Pubky to existing projects +- βœ… **USE MCP**: `adapt_to_project` - Generate project-specific integration code +- βœ… **USE MCP**: `setup_project_dependencies` - Add Pubky dependencies +- βœ… **USE MCP**: `setup_pkarr_project` - Add Pkarr dependencies + +### πŸ§ͺ Testing & Development Tools +- βœ… **USE MCP**: `start_testnet` - Start local testnet +- βœ… **USE MCP**: `stop_testnet` - Stop testnet +- βœ… **USE MCP**: `check_testnet_status` - Check if testnet is running +- βœ… **USE MCP**: `get_testnet_info` - Get testnet connection details +- βœ… **USE MCP**: `start_pkarr_relay` - Start Pkarr relay for development +- βœ… **USE MCP**: `stop_pkarr_relay` - Stop Pkarr relay + +### πŸ”§ Debugging & Troubleshooting +- βœ… **USE MCP**: `explain_capabilities` - Parse capability strings (permissions) +- βœ… **USE MCP**: `verify_installation` - Verify tool installations +- βœ… **USE MCP**: `explain_pkarr_key` - Analyze public keys +- βœ… **USE MCP**: `explain_nexus_component` - Nexus component details + +### πŸ“¦ Installation & Binary Management +- βœ… **USE MCP**: `install_pubky_testnet` - Install testnet binary +- βœ… **USE MCP**: `install_pkarr_relay` - Install Pkarr relay +- βœ… **USE MCP**: `install_pkdns` - Install Pkdns resolver +- βœ… **USE MCP**: `ensure_dependencies` - Smart dependency installer + +### 🌐 DNS & Browser Setup +- βœ… **USE MCP**: `setup_pkdns_browser` - Configure browser for Pkarr domains +- βœ… **USE MCP**: `setup_pkdns_system` - Configure system DNS + +### πŸ—οΈ Advanced (Nexus Development) +- βœ… **USE MCP**: `setup_nexus_dev` - Set up Nexus dev environment +- βœ… **USE MCP**: `explain_nexus_component` - Watcher, service, common components + +--- + +## Examples of CORRECT Agent Behavior + +### βœ… Example 1: Understanding Pubky +**User**: "What is a Pubky homeserver?" + +**CORRECT Agent Response**: +``` +1. Use MCP tool: get_pubky_concept("homeserver") +2. Provide information from MCP +3. No web search needed +``` + +### βœ… Example 2: Creating an App +**User**: "Create a new Pubky app in TypeScript with authentication" + +**CORRECT Agent Response**: +``` +1. Use MCP tool: generate_app_scaffold(projectName, "typescript", ["auth"]) +2. Provide generated code and instructions +3. No web search needed +``` + +### βœ… Example 3: Building Social Features +**User**: "How do I fetch a user's feed from Nexus?" + +**CORRECT Agent Response**: +``` +1. Use MCP tool: query_nexus_api("feed") +2. Use MCP tool: explain_nexus_endpoint("stream_posts_handler") +3. Provide code example from MCP +4. No web search needed +``` + +### βœ… Example 4: Data Validation +**User**: "I need to create a post. What's the format?" + +**CORRECT Agent Response**: +``` +1. Use MCP tool: explain_model("post") +2. Use MCP tool: create_model_example("post", "javascript") +3. Provide model schema and validation rules +4. No web search needed +``` + +### βœ… Example 5: Debugging Capabilities +**User**: "Getting 403 error when writing to /pub/my-app/data.json" + +**CORRECT Agent Response**: +``` +1. Use MCP tool: explain_capabilities("/pub/my-app/:rw") +2. Provide corrected capability string +3. Provide code example +4. No web search needed +``` + +### βœ… Example 6: Project Setup +**User**: "Set up my React project for Pubky" + +**CORRECT Agent Response**: +``` +1. Use MCP tool: analyze_project(projectPath) +2. Use MCP tool: adapt_to_project(projectPath) +3. Use MCP tool: setup_project_dependencies(projectPath) +4. Provide integration code specific to React +5. No web search needed +``` + +### βœ… Example 7: Pkarr Discovery +**User**: "How do I publish DNS records with Pkarr?" + +**CORRECT Agent Response**: +``` +1. Use MCP tool: get_pkarr_concept("discovery") +2. Use MCP tool: get_pkarr_example("javascript", "publish") +3. Use MCP tool: generate_pkarr_client("javascript", includePublish=true) +4. Provide working code +5. No web search needed +``` + +--- + +## Examples of INCORRECT Agent Behavior + +### ❌ Example 1: Going Straight to Web Search +**User**: "What is Pubky?" + +**INCORRECT Agent Response**: +``` +❌ Searching the web for "Pubky protocol"... +``` + +**WHY WRONG**: MCP has comprehensive Pubky documentation. Use `search_documentation("overview")` or `get_pubky_concept("homeserver")` first. + +--- + +### ❌ Example 2: Guessing Instead of Using Tools +**User**: "How do I authenticate with Pubky?" + +**INCORRECT Agent Response**: +``` +❌ Based on common authentication patterns, you probably need to... +``` + +**WHY WRONG**: MCP has exact auth flows. Use `get_pubky_concept("auth")` or `get_code_example("javascript", "auth_flow")`. + +--- + +### ❌ Example 3: Using Web Search for API Questions +**User**: "What Nexus API endpoints are available for posts?" + +**INCORRECT Agent Response**: +``` +❌ Let me search for Nexus API documentation online... +``` + +**WHY WRONG**: MCP has the complete Nexus API spec. Use `query_nexus_api("posts")` or `explain_nexus_endpoint("post_view_handler")`. + +--- + +### ❌ Example 4: Manual Code Writing +**User**: "Generate a Pkarr client for me" + +**INCORRECT Agent Response**: +``` +❌ Here's a Pkarr client I wrote from scratch... +``` + +**WHY WRONG**: MCP can generate validated, working client code. Use `generate_pkarr_client("typescript", includePublish=true, includeResolve=true)`. + +--- + +### ❌ Example 5: Ignoring Data Model Specs +**User**: "I want to create a post object" + +**INCORRECT Agent Response**: +``` +❌ You can create an object like { content: "...", author: "..." } +``` + +**WHY WRONG**: Pubky has validated data models. Use `explain_model("post")` and `create_model_example("post", "javascript")` to ensure interoperability. + +--- + +## When Web Search IS Appropriate + +Use web search **ONLY** when: + +1. ❌ The MCP tools return "not found" or "no information available" +2. ❌ You **tried** something from Pubky MCP and **it's not working** (errors, bugs, edge cases) +3. ❌ You need information about **external tools** (e.g., "How to install Rust?") +4. ❌ You need **current status** of public Pubky services (e.g., "Is nexus.pubky.app down?") +5. ❌ You need **community resources** not in official docs (e.g., "Pubky community forum") +6. ❌ The question is about **non-Pubky topics** (e.g., "How does React Context work?") + +### βœ… Correct Web Search Example 1: Troubleshooting MCP Failures +**User**: "The code from MCP's `generate_app_scaffold` isn't working - getting module not found errors" + +**CORRECT Agent Response**: +``` +1. Use MCP tool: generate_app_scaffold(...) +2. Try the generated code +3. If it fails with errors β†’ NOW search the web for troubleshooting +4. Web search is appropriate here because MCP's solution didn't work +``` + +### βœ… Correct Web Search Example 2: External Tools +**User**: "Is Rust installed on my system?" + +**CORRECT Agent Response**: +``` +1. Use MCP tool: detect_environment() +2. If Rust not found, provide installation link from web +3. Web search is appropriate here for installation guides +``` + +--- + +## Key Architecture Concepts (From MCP) + +### Read vs Write Separation + +**WRITING data (Your App β†’ Your Homeserver):** +- Write posts, profiles, follows to **YOUR homeserver** +- Use `PUT`, `POST`, `DELETE` HTTP methods +- Use `pubky-app-specs` format (validate with MCP: `validate_model_data`) +- **Never write to Nexus** - it's read-only! + +**READING social data (Your App β†’ Nexus):** +- Query feeds, search, discovery from **Nexus API** +- Use `GET` HTTP methods +- Nexus has pre-indexed all public data (~0.5s response) +- Generate client with MCP: `generate_nexus_client` + +**READING your own data (Your App β†’ Your Homeserver):** +- Get your private data from **your homeserver** +- Direct HTTP `GET` to your storage + +Use MCP to understand: `get_nexus_architecture` + +--- + +## MCP Resources Available + +The MCP also provides **Resources** (read-only docs) via the resources panel: + +- `pubky://docs/*` - Core protocol documentation +- `pubky://examples/*` - Code examples (Rust & JS) +- `pubky://api/nexus/*` - Nexus API specifications +- `pubky://specs/models/*` - Data model specs + +**Access these when you need to read documentation directly.** + +--- + +## Summary: Agent Workflow + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ User asks about Pubky β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ 1. Check Available MCP Toolsβ”‚ ← ALWAYS DO THIS FIRST +β”‚ - get_pubky_concept β”‚ +β”‚ - search_documentation β”‚ +β”‚ - query_nexus_api β”‚ +β”‚ - generate_data_model β”‚ +β”‚ - get_code_example β”‚ +β”‚ - etc. (65+ tools) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ 2. Use MCP Tool(s) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ 3. Did MCP provide answer? β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β” + β”‚ β”‚ + YES NO + β”‚ β”‚ + β–Ό β–Ό + β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚TEST β”‚ β”‚Web Searchβ”‚ + β””β”€β”€β”¬β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ 4. Does MCP solution work? β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β” + β”‚ β”‚ + YES NO + β”‚ β”‚ + β–Ό β–Ό + β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚DONE β”‚ β”‚Web Searchβ”‚ + β””β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## Final Reminder + +🎯 **The Pubky MCP server is comprehensive and authoritative.** + +🎯 **Always try MCP first - it has 65+ tools covering all aspects of Pubky development.** + +🎯 **Web search should be a last resort, not the first step.** + +🎯 **If MCP solution doesn't work (errors, bugs), THEN search the web for troubleshooting.** + +🎯 **When in doubt, use `search_documentation` to find the right MCP tool.** + +--- + +**Made for Cursor AI and other MCP-compatible agents.** +**Last updated**: October 2025 + diff --git a/pubky-mcp-server/LICENSE b/pubky-mcp-server/LICENSE new file mode 100644 index 0000000..dc707f4 --- /dev/null +++ b/pubky-mcp-server/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2025 Pubky Team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/pubky-mcp-server/QUICKSTART.md b/pubky-mcp-server/QUICKSTART.md new file mode 100644 index 0000000..a231520 --- /dev/null +++ b/pubky-mcp-server/QUICKSTART.md @@ -0,0 +1,168 @@ +# Quick Start Guide + +Get your Pubky MCP Server running in 3 minutes! + +## 1. Build (if not already done) + +```bash +cd pubky-mcp-server +npm install +npm run build # Automatically fetches all resources +``` + +## 2. Configure Cursor + +Edit your Cursor MCP config file (`~/.cursor/mcp.json`): + +```json +{ + "mcpServers": { + "pubky": { + "command": "node", + "args": ["/absolute/path/to/hackathon-2025/pubky-mcp-server/dist/index.js"] + } + } +} +``` + +**Note:** No environment variables needed! All resources are bundled during build. + +## 3. Restart Cursor + +Close and reopen Cursor to load the MCP server. + +## 4. Test It! + +Open Cursor and try these commands: + +### Test 1: Understand the Architecture + +``` +"Explain the Pubky architecture with all 4 layers" +``` + +Should explain Pkarr (discovery), Pubky Core (protocol), App Specs (data models), and Nexus (social API). + +### Test 2: Pkarr Discovery + +``` +"How do I resolve a public key to find the homeserver using Pkarr?" +``` + +The MCP server will explain Pkarr's discovery layer. + +### Test 3: Get Examples + +``` +"Show me how to publish DNS records with Pkarr in JavaScript" +``` + +Will show Pkarr code examples. + +### Test 4: Start Development Environment + +``` +"Start the Pubky testnet for local development" +``` + +Starts local testnet with DHT, homeserver, and Pkarr relay. + +### Test 5: Build an App + +``` +"Create a new Pubky social app called 'my-app' in TypeScript" +``` + +Generates a complete app scaffold. + +## What Can You Do? + +### Layer 1: Discovery + +**Pkarr (Protocol)** +- "Explain how Pkarr discovery works" +- "Generate a Pkarr client to publish DNS records" +- "Show me how to resolve a public key" +- "Start a local Pkarr relay" +- "Generate code to create Pkarr keypairs" + +**Pkdns (DNS Resolver)** +- "How do I set up pkdns in my browser?" +- "Configure my system DNS to use pkdns" +- "Install pkdns server" +- "Show me public pkdns servers" + +### Layer 2: Pubky Core (Protocol) + +- "Explain Pubky capabilities" +- "Show me the authentication flow" +- "What is a root key?" +- "How do I store data on a homeserver?" + +### Layer 3: App Specs (Data Models) + +- "Show me how to create a post with validation" +- "Explain the User model" +- "Generate code for PubkyAppPost" +- "Validate my post data" + +### Layer 4: Social Graph + +**Nexus API (Interface)** +- "Show me all Nexus API endpoints" +- "How do I query a social feed?" +- "Generate a Nexus API client" +- "Explain the UserView schema" + +**Nexus Implementation (For Advanced Users)** +- "Explain Nexus architecture" +- "How do I set up Nexus development environment?" +- "Explain the Nexus watcher component" +- "Show me how to run Nexus locally" + +### Development Tools + +- "Create a new Pubky app" +- "Start the testnet" +- "Add Pubky to my existing React app" +- "Check if I have all tools installed" + +## Troubleshooting + +### "MCP server not found" + +1. Check the path in your config is correct (use absolute path) +2. Make sure you ran `npm run build` +3. Restart Cursor completely + +### "Resources not found" + +Run `npm run build` to fetch all resources (Pkarr, Pubky Core, App Specs, Nexus). + +### "Tool execution failed" + +Most tools provide error messages. Common fixes: + +- Install missing dependencies (`cargo`, `node`, etc.) +- Start the testnet if needed: "Start testnet" +- For Pkarr relay tools: Ensure Rust/Cargo is installed + +## Next Steps + +1. **Explore the 4 Layers**: Ask about each layer (Pkarr, Core, App Specs, Nexus) +2. **Browse Resources**: Access documentation via MCP resources panel +3. **Try Tools**: Experiment with 55+ development tools +4. **Build Something**: Create your first Pubky app! + +## Pro Tips + +- The MCP server is context-aware - it can analyze your current project and adapt suggestions +- Use natural language – no need to specify tool names +- Chain requests: "Create a new app, start testnet, and add auth" +- The server has access to all Pubky documentation and examples + +--- + +**Happy Building! πŸš€** + +Need help? Just ask the MCP server: "How do I use the Pubky MCP server?" diff --git a/pubky-mcp-server/README.md b/pubky-mcp-server/README.md new file mode 100644 index 0000000..2c0f5ba --- /dev/null +++ b/pubky-mcp-server/README.md @@ -0,0 +1,426 @@ +# Pubky MCP Server + +Your complete Pubky ecosystem expert! This Model Context Protocol (MCP) server provides comprehensive knowledge, code examples, and development tools for building applications on the Pubky protocol. + +## Features + +πŸ”Ί **Complete Pubky Ecosystem Coverage** + +The MCP provides expertise on **all 6 official Pubky projects** across 4 layers: + +**Layer 1: Discovery** +- **Pkarr** (pkarr): Public-Key Addressable Resource Records - DHT-based discovery +- **Pkdns** (pkdns): DNS resolver that makes Pkarr domains work as TLDs in browsers + +**Layer 2: Protocol** +- **Pubky Core** (pubky-core): Auth, storage, homeserver protocol + +**Layer 3: Data Models** +- **App Specs** (pubky-app-specs): Validated data models (User, Post, Tag, etc.) + +**Layer 4: Social Graph** +- **Nexus API** (nexus-webapi): REST API for reading social data +- **Nexus** (pubky-nexus): Graph indexer implementation (watcher, service, databases) + +πŸ› οΈ **Smart Development Tools (65+ tools)** + +- **Discovery**: Pkarr client generation, DNS record builders, keypair management +- **DNS Setup**: Pkdns browser/system configuration, server installation +- **Protocol**: Code generation, project scaffolding, auth flows +- **Data Models**: Validation, examples, schema generation +- **Social API**: Nexus client generation, endpoint exploration +- **Infrastructure**: Testnet management, Pkarr relay, Nexus dev setup +- **Environment**: Dependency management, project integration + +πŸ“š **Rich Code Examples** + +- Rust and JavaScript/TypeScript examples +- Pre-built templates for common patterns +- Adaptive code generation based on your project +- Full-stack social app examples + +πŸ’‘ **Interactive Guides** + +- Discovery: Pkarr resolution, DNS setup, domain publishing +- Protocol: Authentication, storage, capabilities, testnet +- Social Features: Feeds, posts, user profiles, Nexus architecture +- Data Validation: Model specs and validation rules +- Advanced: Nexus development, graph databases, indexing + +## Installation + +### Prerequisites + +- Node.js 20+ (for the MCP server itself) +- Rust/Cargo (optional, for Pubky development) + +### Install Dependencies + +```bash +cd pubky-mcp-server +npm install +``` + +### Build + +```bash +npm run build +``` + +## Configuration + +### For Cursor + +**Option 1: Using npx (Easiest - No installation needed)** + +Add to your Cursor settings (Settings β†’ Features β†’ MCP): + +```json +{ + "mcpServers": { + "pubky": { + "command": "npx", + "args": ["@pubky/mcp-server"] + } + } +} +``` + +**That's it!** All resources are bundled - no environment variables needed. + +**Option 2: Global Installation** + +```bash +npm install -g @pubky/mcp-server +``` + +Then configure: + +```json +{ + "mcpServers": { + "pubky": { + "command": "pubky-mcp" + } + } +} +``` + +**Option 3: Local Development** + +If you're developing the MCP server itself: + +```json +{ + "mcpServers": { + "pubky": { + "command": "node", + "args": ["/absolute/path/to/pubky-hackathon/pubky-mcp-server/dist/index.js"] + } + } +} +``` + +### What's Bundled? + +The package includes everything you need: +- βœ… **Pubky Core docs & examples** (from https://github.com/pubky/pubky-core) +- βœ… **Nexus API specification** (from https://nexus.pubky.app/api-docs/v0/openapi.json) +- βœ… **Pubky App Specs** (from https://github.com/pubky/pubky-app-specs) + +**No cloning repos. No environment variables. Just works!** + +## Usage Examples + +Once connected in Cursor, you can interact naturally: + +### Example 1: Creating a New App + +**You:** "Create a new Pubky app in JavaScript with authentication" + +**MCP Server:** + +- Uses `generate_app_scaffold` tool +- Creates project with auth flow code +- Provides setup instructions +- Suggests testnet for development + +### Example 2: Understanding Concepts + +**You:** "Explain what a Pubky homeserver is" + +**MCP Server:** + +- Uses `get_pubky_concept` tool +- Returns detailed explanation +- Provides code examples +- Links to relevant documentation + +### Example 3: Debugging + +**You:** "I'm getting a 403 error when trying to write to /pub/my-app/data.json" + +**MCP Server:** + +- Uses `explain_capabilities` tool +- Analyzes your capability string +- Suggests fixes +- Provides corrected code + +### Example 4: Environment Setup + +**You:** "Set up my project for Pubky development" + +**MCP Server:** + +- Uses `detect_environment` tool +- Uses `analyze_project` tool +- Installs missing dependencies +- Configures project files +- Starts testnet if needed + +## Available Resources + +Access via the resources panel in Cursor (25 total): + +**Protocol Documentation:** +- `pubky://docs/overview` - Protocol overview +- `pubky://docs/concepts/homeserver` - Homeserver concept +- `pubky://docs/concepts/rootkey` - Root key concept +- `pubky://docs/auth` - Authentication specification +- `pubky://examples/rust/all` - All Rust examples +- `pubky://examples/javascript/all` - All JavaScript examples +- `pubky://api/homeserver` - Homeserver API reference +- `pubky://api/sdk` - SDK API documentation +- `pubky://api/capabilities` - Capabilities reference + +**Nexus API (7 resources):** +- `pubky://api/nexus/overview` - API overview +- `pubky://api/nexus/endpoints/posts` - Post endpoints +- `pubky://api/nexus/endpoints/users` - User endpoints +- `pubky://api/nexus/endpoints/streams` - Stream endpoints +- `pubky://api/nexus/endpoints/search` - Search endpoints +- `pubky://api/nexus/endpoints/tags` - Tag endpoints +- `pubky://api/nexus/schemas` - All 42 data schemas + +**Pubky App Specs (9 resources):** +- `pubky://specs/overview` - Data models overview +- `pubky://specs/models/user` - PubkyAppUser model +- `pubky://specs/models/post` - PubkyAppPost model +- `pubky://specs/models/tag` - PubkyAppTag model +- `pubky://specs/models/bookmark` - PubkyAppBookmark model +- `pubky://specs/models/follow` - PubkyAppFollow model +- `pubky://specs/models/file` - PubkyAppFile model +- `pubky://specs/models/feed` - PubkyAppFeed model +- `pubky://specs/examples` - JavaScript examples + +## Available Tools + +The server provides 28 tools: + +### Documentation & Learning + +- `get_pubky_concept` - Explain concepts +- `get_code_example` - Get specific examples +- `search_documentation` - Search all docs +- `explain_capabilities` - Parse capability strings + +### Code Generation + +- `generate_app_scaffold` - Create new projects +- `get_code_template` - Get code templates +- `list_templates` - Show available templates + +### Environment & Analysis + +- `analyze_project` - Analyze project structure +- `detect_environment` - Check installed tools +- `suggest_setup` - Recommend setup steps + +### Dependency Management + +- `ensure_dependencies` - Smart dependency installer +- `install_pubky_testnet` - Install testnet binary +- `setup_project_dependencies` - Add Pubky to projects +- `verify_installation` - Check tool installations + +### Testnet Management + +- `start_testnet` - Start local testnet +- `stop_testnet` - Stop testnet +- `restart_testnet` - Restart testnet +- `check_testnet_status` - Check if running +- `get_testnet_info` - Get connection details + +### Integration + +- `adapt_to_project` - Generate adaptive integration code +- `integrate_pubky` - Add Pubky to existing projects + +### Nexus API Tools (NEW) + +- `query_nexus_api` - Search Nexus endpoints +- `explain_nexus_endpoint` - Endpoint details with examples +- `generate_nexus_client` - Auto-generate API clients + +### Pubky App Specs Tools (NEW) + +- `generate_data_model` - Generate model code +- `validate_model_data` - Validate against specs +- `explain_model` - Model documentation +- `create_model_example` - Working code examples + +## Available Prompts + +Interactive guides for common tasks (10 total): + +**Protocol Guides:** +- `create-pubky-app` - Create a new application +- `implement-auth` - Add authentication +- `add-storage` - Add storage operations +- `debug-capabilities` - Debug permission issues +- `setup-testnet` - Set up local testnet + +**Social Features Guides (NEW):** +- `build-social-feed` - Build social feeds +- `create-post-ui` - Post creation with validation +- `implement-user-profile` - User profile management +- `query-social-data` - Query Nexus API +- `validate-app-data` - Data validation + +## Development + +### Running in Development Mode + +```bash +npm run dev +``` + +### File Structure + +``` +pubky-mcp-server/ +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ index.ts # Main server entry point +β”‚ β”œβ”€β”€ resources.ts # Resource handlers (3-layer architecture) +β”‚ β”œβ”€β”€ tools.ts # Tool implementations (28 tools) +β”‚ β”œβ”€β”€ prompts.ts # Prompt templates (10 guides) +β”‚ β”œβ”€β”€ types.ts # TypeScript types +β”‚ β”œβ”€β”€ constants.ts # Constants and enums +β”‚ └── utils/ +β”‚ β”œβ”€β”€ file-reader.ts # File system utilities +β”‚ β”œβ”€β”€ testnet.ts # Testnet management +β”‚ β”œβ”€β”€ environment.ts # Environment detection +β”‚ β”œβ”€β”€ templates.ts # Code templates +β”‚ β”œβ”€β”€ nexus-api.ts # Nexus API parser (NEW) +β”‚ └── app-specs.ts # App specs parser (NEW) +β”œβ”€β”€ dist/ # Compiled JavaScript +β”œβ”€β”€ package.json +β”œβ”€β”€ tsconfig.json +β”œβ”€β”€ cursor-config-example.json +└── README.md +``` + +## How It Works + +The MCP server acts as a bridge between Cursor (or any MCP client) and the complete Pubky ecosystem: + +1. **Resources**: Read-only access to documentation, API specs, and data models +2. **Tools**: Executable functions that perform actions (generate code, validate data, query APIs, manage testnet) +3. **Prompts**: Pre-configured conversation starters for common development tasks + +All communication happens via stdio, making it fast and reliable. + +## Architecture Overview + +Pubky uses a **hub-spoke model** where your app interacts with different components for different purposes: + +``` + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ NEXUS β”‚ ← Crawls/indexes public data + β”‚ (Indexer) β”‚ from all homeservers (~0.5s) + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β–² + β”‚ GET (read social data) + β”‚ feeds, search, discovery + β”‚ + β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β” + β”‚ Your β”‚ + β”‚ App β”‚ + β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ + β”‚ + β”‚ PUT/DELETE (write your data) + β”‚ following pubky-app-specs format + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Your Homeserver β”‚ ← Your personal storage + β”‚ (pubky-core) β”‚ Authentication & auth + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Data Flow + +**Writing Data (You β†’ Your Homeserver):** +- Create a post β†’ `PUT` to your homeserver at `/pub/pubky.app/posts/{id}` +- Update profile β†’ `PUT` to your homeserver at `/pub/pubky.app/profile.json` +- Delete content β†’ `DELETE` from your homeserver +- βœ… Use `pubky-app-specs` format for interoperability +- ❌ Nexus is NOT involved in writes + +**Reading Social Data (You β†’ Nexus):** +- View feed β†’ `GET` from Nexus API (`/v0/stream/...`) +- Search users β†’ `GET` from Nexus API (`/v0/search/users`) +- Discover posts β†’ `GET` from Nexus API +- βœ… Nexus has already crawled and indexed all public data +- ❌ Don't query other homeservers directly (slow!) + +**Reading Your Own Data (You β†’ Your Homeserver):** +- Get your own post β†’ `GET` from your homeserver +- List your files β†’ `GET` from your homeserver +- Private data only you can access + +## Troubleshooting + +### Server Not Connecting + +1. Check that the path in your MCP config is correct +2. Ensure the server is built: `npm run build` +3. Check Cursor's MCP logs for error messages + +### Bundled Resources Not Found + +If you see "Bundled resources not found", you're likely running from source without building: + +```bash +cd pubky-mcp-server +npm run build # Fetches resources and compiles +``` + +### Resources Outdated + +To refresh bundled resources: + +```bash +npm run fetch-resources # Re-download latest public resources +npm run build:quick # Recompile without fetching +``` + +### Tool Execution Errors + +Most tools will provide helpful error messages. Common issues: + +- Missing dependencies (use `detect_environment` tool) +- Invalid project paths +- Testnet not running (use `start_testnet` tool) + +## Contributing + +This MCP server is part of the Pubky Core project. Contributions welcome! + +## License + +MIT + +--- + +**Made with ❀️ for Pubky developers** diff --git a/pubky-mcp-server/SETUP.md b/pubky-mcp-server/SETUP.md new file mode 100644 index 0000000..f9eee37 --- /dev/null +++ b/pubky-mcp-server/SETUP.md @@ -0,0 +1,178 @@ +# Quick Setup Guide + +## Zero Configuration Setup + +The Pubky MCP Server now **bundles all resources** - no environment variables needed! + +## Understanding Pubky Architecture First + +Before diving in, understand the complete 4-layer Pubky stack: + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Layer 4: NEXUS (Social Indexer) β”‚ +β”‚ WHERE to READ: Aggregated feeds, search, discovery β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ READ (via Nexus API) + β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Layer 3: APP SPECS (Data Models) β”‚ +β”‚ WHAT format: User, Post, Tag, etc. with validation β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ Format data + β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Layer 2: PUBKY CORE (Protocol) β”‚ +β”‚ HOW to access: Auth, storage, capabilities β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ WRITE (to homeserver) + β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Layer 1: PKARR (Discovery) β”‚ +β”‚ WHERE is homeserver: Resolve pubkey β†’ homeserver URL β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Complete Flow:** +1. **Pkarr**: Resolve public key to find homeserver location +2. **Pubky Core**: Connect and authenticate with homeserver +3. **App Specs**: Format data (User, Post, etc.) +4. **Nexus**: Read aggregated social data from all users + +--- + +## Installation Options + +All you need is one of these options: + +### Option 1: npx (Fastest - No Installation) + +1. Open Cursor Settings (Cmd+,) +2. Go to **Features** β†’ **MCP** +3. Click **Edit Config** +4. Add this: + +```json +{ + "mcpServers": { + "pubky": { + "command": "npx", + "args": ["@pubky/mcp-server"] + } + } +} +``` + +5. Save and restart Cursor +6. **Done!** No installation, no configuration needed. + +### Option 2: Global Installation + +```bash +npm install -g @pubky/mcp-server +``` + +Configure Cursor: + +```json +{ + "mcpServers": { + "pubky": { + "command": "pubky-mcp" + } + } +} +``` + +### Option 3: Local Development + +If you're working on the MCP server code itself: + +```bash +cd pubky-mcp-server +npm install +npm run build # Fetches resources and compiles +``` + +Configure Cursor: + +```json +{ + "mcpServers": { + "pubky": { + "command": "node", + "args": ["/absolute/path/to/pubky-hackathon/pubky-mcp-server/dist/index.js"] + } + } +} +``` + +## Verify It's Working + +After setup, the server should display: + +``` +βœ… Pubky MCP Server running + +πŸ”Ί Complete Pubky Stack (4 Layers): + β€’ Layer 1: Pkarr (pkarr) - WHERE is the homeserver? (Discovery via DHT) + β€’ Layer 2: Pubky Core (pubky-core) - HOW to read/write homeserver + β€’ Layer 3: App Specs (pubky-app-specs) - WHAT format to use + β€’ Layer 4: Nexus (nexus-webapi) - WHERE you READ aggregated social data +``` + +## Test All 4 Layers + +Try these commands in Cursor: + +1. **"Explain how Pkarr discovery works"** - Layer 1: Discovery via DHT +2. **"Show me Pubky authentication flow"** - Layer 2: Protocol & auth +3. **"How do I create a validated post?"** - Layer 3: Data models +4. **"Show me all Nexus API endpoints"** - Layer 4: Social indexer +5. **"Generate a complete Pubky app with all layers"** - Full stack integration + +## What You Get + +**Bundled in every installation:** + +- βœ… **75+ Resources**: Complete documentation across all 6 projects + - 13 Pkarr resources (discovery protocol, DHT, relay) + - 5 Pkdns resources (DNS resolver, setup guides) + - 9 Pubky Core resources (protocol, auth, storage) + - 9 App Specs resources (data models, validation) + - 7 Nexus API resources (REST endpoints, schemas) + - 7 Nexus impl resources (architecture, components, dev setup) + +- βœ… **65+ Tools**: Full development toolkit + - 14 Pkarr tools (discovery, DNS records, relay management) + - 4 Pkdns tools (browser setup, system DNS, installation) + - 21 Pubky Core tools (auth, storage, testnet) + - 4 App Specs tools (models, validation) + - 3 Nexus API tools (query, explain, generate) + - 3 Nexus impl tools (architecture, dev setup, components) + - 16+ Environment tools (setup, installation, integration) + +- βœ… **11 Prompts**: Interactive guides + - Discovery & DNS (Pkarr, Pkdns) + - Protocol & auth (Pubky Core) + - Social features (feeds, posts, profiles, Nexus) + - Data validation (App Specs) + - Advanced (Nexus development) + +**Zero dependencies on external repos or environment variables!** + +## Troubleshooting + +**"Bundled resources not found"** + +β†’ Run `npm run build` to fetch and bundle resources + +**"Resources seem outdated"** + +β†’ Run `npm run fetch-resources` to refresh from public sources + +**Server won't start** + +β†’ Make sure you've run `npm install` and `npm run build` + + diff --git a/pubky-mcp-server/cursor-config-example.json b/pubky-mcp-server/cursor-config-example.json new file mode 100644 index 0000000..ff1d183 --- /dev/null +++ b/pubky-mcp-server/cursor-config-example.json @@ -0,0 +1,12 @@ +{ + "mcpServers": { + "pubky": { + "command": "node", + "args": ["/absolute/path/to/hackathon-2025/pubky-mcp-server/dist/index.js"] + } + } +} + +// Note: No environment variables needed! +// All resources (Pkarr, Pkdns, Pubky Core, App Specs, Nexus) +// are automatically fetched and bundled during 'npm run build' diff --git a/pubky-mcp-server/package.json b/pubky-mcp-server/package.json new file mode 100644 index 0000000..a24fa8c --- /dev/null +++ b/pubky-mcp-server/package.json @@ -0,0 +1,72 @@ +{ + "name": "@pubky/mcp-server", + "version": "1.0.0", + "description": "Complete Pubky ecosystem MCP server - All 6 official projects (Pkarr, Pkdns, Pubky Core, App Specs, Nexus API, Nexus) with 75+ resources and 65+ dev tools", + "type": "module", + "main": "dist/index.js", + "bin": { + "pubky-mcp": "dist/index.js" + }, + "files": [ + "dist", + "data", + "README.md", + "SETUP.md", + "LICENSE" + ], + "scripts": { + "fetch-resources": "bash scripts/fetch-resources.sh", + "build": "npm run fetch-resources && tsc", + "build:quick": "tsc", + "postinstall": "npm run build", + "watch": "tsc --watch", + "start": "node dist/index.js", + "dev": "npm run build:quick && node dist/index.js", + "prepublishOnly": "npm run build", + "lint": "oxlint", + "lint:fix": "oxlint --fix", + "format": "prettier --write .", + "format:check": "prettier --check .", + "check": "npm run lint && npm run format:check", + "fix": "npm run lint:fix && npm run format" + }, + "keywords": [ + "pubky", + "pkarr", + "pkdns", + "pubky-core", + "pubky-app-specs", + "pubky-nexus", + "nexus-api", + "mcp", + "model-context-protocol", + "decentralized", + "social", + "dht", + "dns", + "web3", + "ai-assistant", + "cursor" + ], + "author": "Pubky Team", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/pubky/pubky-hackathon.git", + "directory": "pubky-mcp-server" + }, + "homepage": "https://github.com/pubky/pubky-hackathon/tree/main/pubky-mcp-server#readme", + "bugs": { + "url": "https://github.com/pubky/pubky-hackathon/issues" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.4", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "oxlint": "^1.23.0", + "prettier": "^3.6.2", + "typescript": "^5.7.2" + } +} diff --git a/pubky-mcp-server/scripts/fetch-resources.sh b/pubky-mcp-server/scripts/fetch-resources.sh new file mode 100755 index 0000000..b3c5e14 --- /dev/null +++ b/pubky-mcp-server/scripts/fetch-resources.sh @@ -0,0 +1,189 @@ +#!/bin/bash + +# Pubky MCP Server - Fetch Public Resources +# This script downloads all required resources from public URLs + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DATA_DIR="$SCRIPT_DIR/../data" + +echo "πŸš€ Fetching public Pubky resources..." +echo "" + +# Create data directories +mkdir -p "$DATA_DIR/pubky-core" +mkdir -p "$DATA_DIR/pubky-app-specs" +mkdir -p "$DATA_DIR/pkarr" +mkdir -p "$DATA_DIR/pkdns" +mkdir -p "$DATA_DIR/pubky-nexus" + +# 1. Fetch Nexus Web API OpenAPI spec +echo "πŸ“‘ Fetching Nexus Web API specification..." +curl -fsSL https://nexus.pubky.app/api-docs/v0/openapi.json -o "$DATA_DIR/nexus-webapi.json" +echo "βœ… Nexus API spec downloaded" +echo "" + +# 2. Clone pubky-core (docs and examples only) +echo "πŸ“š Fetching pubky-core documentation and examples..." +if [ -d "$DATA_DIR/pubky-core/.git" ]; then + echo " Updating existing pubky-core..." + cd "$DATA_DIR/pubky-core" + git pull origin main --depth 1 + cd - > /dev/null +else + echo " Cloning pubky-core..." + git clone --depth 1 --filter=blob:none --sparse https://github.com/pubky/pubky-core.git "$DATA_DIR/pubky-core" + cd "$DATA_DIR/pubky-core" + git sparse-checkout set docs examples + cd - > /dev/null +fi +echo "βœ… pubky-core resources downloaded" +echo "" + +# 3. Clone pubky-app-specs +echo "πŸ“‹ Fetching pubky-app-specs..." +if [ -d "$DATA_DIR/pubky-app-specs/.git" ]; then + echo " Updating existing pubky-app-specs..." + cd "$DATA_DIR/pubky-app-specs" + git pull origin main --depth 1 + cd - > /dev/null +else + echo " Cloning pubky-app-specs..." + git clone --depth 1 https://github.com/pubky/pubky-app-specs.git "$DATA_DIR/pubky-app-specs" +fi +echo "βœ… pubky-app-specs downloaded" +echo "" + +# 4. Clone pkarr +echo "πŸ” Fetching pkarr..." +if [ -d "$DATA_DIR/pkarr/.git" ]; then + echo " Updating existing pkarr..." + cd "$DATA_DIR/pkarr" + git pull origin main --depth 1 + cd - > /dev/null +else + echo " Cloning pkarr..." + git clone --depth 1 --filter=blob:none --sparse https://github.com/pubky/pkarr.git "$DATA_DIR/pkarr" + cd "$DATA_DIR/pkarr" + git sparse-checkout set design pkarr bindings relay + cd - > /dev/null +fi +echo "βœ… pkarr downloaded" +echo "" + +# 5. Clone pkdns +echo "🌐 Fetching pkdns (DNS resolver)..." +if [ -d "$DATA_DIR/pkdns/.git" ]; then + echo " Updating existing pkdns..." + cd "$DATA_DIR/pkdns" + git pull origin master --depth 1 + cd - > /dev/null +else + echo " Cloning pkdns..." + git clone --depth 1 --filter=blob:none --sparse --branch master https://github.com/pubky/pkdns.git "$DATA_DIR/pkdns" + cd "$DATA_DIR/pkdns" + git sparse-checkout set docs cli server + cd - > /dev/null +fi +echo "βœ… pkdns downloaded" +echo "" + +# 6. Clone pubky-nexus +echo "πŸ“‘ Fetching pubky-nexus (social indexer)..." +if [ -d "$DATA_DIR/pubky-nexus/.git" ]; then + echo " Updating existing pubky-nexus..." + cd "$DATA_DIR/pubky-nexus" + git pull origin main --depth 1 + cd - > /dev/null +else + echo " Cloning pubky-nexus..." + git clone --depth 1 --filter=blob:none --sparse https://github.com/pubky/pubky-nexus.git "$DATA_DIR/pubky-nexus" + cd "$DATA_DIR/pubky-nexus" + git sparse-checkout set docs examples nexus-common nexus-watcher nexus-webapi + cd - > /dev/null +fi +echo "βœ… pubky-nexus downloaded" +echo "" + +# 7. Verify all files are present +echo "πŸ” Verifying downloaded resources..." + +if [ ! -f "$DATA_DIR/nexus-webapi.json" ]; then + echo "❌ Error: nexus-webapi.json not found" + exit 1 +fi + +if [ ! -d "$DATA_DIR/pubky-core/docs" ]; then + echo "❌ Error: pubky-core/docs not found" + exit 1 +fi + +if [ ! -d "$DATA_DIR/pubky-core/examples" ]; then + echo "❌ Error: pubky-core/examples not found" + exit 1 +fi + +if [ ! -d "$DATA_DIR/pubky-app-specs/src" ]; then + echo "❌ Error: pubky-app-specs/src not found" + exit 1 +fi + +if [ ! -f "$DATA_DIR/pubky-app-specs/README.md" ]; then + echo "❌ Error: pubky-app-specs/README.md not found" + exit 1 +fi + +if [ ! -f "$DATA_DIR/pkarr/README.md" ]; then + echo "❌ Error: pkarr/README.md not found" + exit 1 +fi + +if [ ! -d "$DATA_DIR/pkarr/design" ]; then + echo "❌ Error: pkarr/design not found" + exit 1 +fi + +if [ ! -f "$DATA_DIR/pkdns/README.md" ]; then + echo "❌ Error: pkdns/README.md not found" + exit 1 +fi + +if [ ! -d "$DATA_DIR/pkdns/docs" ]; then + echo "❌ Error: pkdns/docs not found" + exit 1 +fi + +if [ ! -f "$DATA_DIR/pubky-nexus/README.md" ]; then + echo "❌ Error: pubky-nexus/README.md not found" + exit 1 +fi + +if [ ! -d "$DATA_DIR/pubky-nexus/docs" ]; then + echo "❌ Error: pubky-nexus/docs not found" + exit 1 +fi + +echo "βœ… All resources verified" +echo "" + +# 8. Show summary +echo "πŸ“Š Resource Summary:" +echo " β€’ Nexus API spec: $(wc -c < "$DATA_DIR/nexus-webapi.json" | xargs) bytes" +echo " β€’ pubky-core docs: $(find "$DATA_DIR/pubky-core/docs" -type f | wc -l | xargs) files" +echo " β€’ pubky-core examples: $(find "$DATA_DIR/pubky-core/examples" -type f | wc -l | xargs) files" +echo " β€’ pubky-app-specs models: $(find "$DATA_DIR/pubky-app-specs/src/models" -name "*.rs" 2>/dev/null | wc -l | xargs) models" +echo " β€’ pkarr design docs: $(find "$DATA_DIR/pkarr/design" -name "*.md" 2>/dev/null | wc -l | xargs) files" +echo " β€’ pkarr examples: $(find "$DATA_DIR/pkarr/pkarr/examples" -name "*.rs" 2>/dev/null | wc -l | xargs) files" +echo " β€’ pkdns docs: $(find "$DATA_DIR/pkdns/docs" -name "*.md" 2>/dev/null | wc -l | xargs) files" +echo " β€’ pubky-nexus docs: $(find "$DATA_DIR/pubky-nexus/docs" -name "*.md" 2>/dev/null | wc -l | xargs) files" +echo " β€’ pubky-nexus examples: $(find "$DATA_DIR/pubky-nexus/examples" -type f -name "*.rs" 2>/dev/null | wc -l | xargs) files" +echo "" + +echo "πŸŽ‰ All resources fetched successfully!" +echo "" +echo "Next steps:" +echo " 1. Run: npm run build" +echo " 2. Test: node dist/index.js" +echo " 3. Publish: npm publish" + diff --git a/pubky-mcp-server/src/constants.ts b/pubky-mcp-server/src/constants.ts new file mode 100644 index 0000000..f320853 --- /dev/null +++ b/pubky-mcp-server/src/constants.ts @@ -0,0 +1,197 @@ +/** + * Constants for the Pubky MCP Server + */ + +// Supported programming languages +export const LANGUAGES = { + RUST: 'rust', + JAVASCRIPT: 'javascript', + TYPESCRIPT: 'typescript', +} as const; + +export type Language = (typeof LANGUAGES)[keyof typeof LANGUAGES]; + +// Resource types +export const RESOURCE_TYPES = { + DOCS: 'docs', + EXAMPLES: 'examples', + API: 'api', + SPECS: 'specs', +} as const; + +export type ResourceType = (typeof RESOURCE_TYPES)[keyof typeof RESOURCE_TYPES]; + +// Documentation sections +export const DOC_SECTIONS = { + OVERVIEW: 'overview', + CONCEPTS: 'concepts', + AUTH: 'auth', +} as const; + +export type DocSection = (typeof DOC_SECTIONS)[keyof typeof DOC_SECTIONS]; + +// Concept types +export const CONCEPT_TYPES = { + HOMESERVER: 'homeserver', + ROOTKEY: 'rootkey', + AUTH: 'auth', + CAPABILITIES: 'capabilities', + STORAGE: 'storage', +} as const; + +export type ConceptType = (typeof CONCEPT_TYPES)[keyof typeof CONCEPT_TYPES]; + +// API sections +export const API_SECTIONS = { + HOMESERVER: 'homeserver', + SDK: 'sdk', + CAPABILITIES: 'capabilities', + NEXUS: 'nexus', +} as const; + +export type ApiSection = (typeof API_SECTIONS)[keyof typeof API_SECTIONS]; + +// Project types +export const PROJECT_TYPES = { + RUST: 'rust', + JAVASCRIPT: 'javascript', + TYPESCRIPT: 'typescript', + UNKNOWN: 'unknown', +} as const; + +export type ProjectType = (typeof PROJECT_TYPES)[keyof typeof PROJECT_TYPES]; + +// Framework types +export const FRAMEWORKS = { + REACT: 'react', + VUE: 'vue', + SVELTE: 'svelte', +} as const; + +export type Framework = (typeof FRAMEWORKS)[keyof typeof FRAMEWORKS]; + +// Capability actions +export const CAPABILITY_ACTIONS = { + READ: 'r', + WRITE: 'w', + READ_WRITE: 'rw', +} as const; + +export type CapabilityAction = (typeof CAPABILITY_ACTIONS)[keyof typeof CAPABILITY_ACTIONS]; + +// File extensions for search +export const SEARCH_FILE_EXTENSIONS = ['.md', '.rs', '.js', '.mjs', '.ts'] as const; + +// Ignored directories for search +export const IGNORED_DIRECTORIES = ['.', 'node_modules', 'target'] as const; + +// Nexus API endpoint categories +export const NEXUS_ENDPOINT_CATEGORIES = { + BOOTSTRAP: 'Bootstrap', + POST: 'Post', + USER: 'User', + STREAM: 'Stream', + SEARCH: 'Search', + TAGS: 'Tags', + TAG: 'Tag', + FILE: 'File', + INFO: 'Info', +} as const; + +export type NexusEndpointCategory = + (typeof NEXUS_ENDPOINT_CATEGORIES)[keyof typeof NEXUS_ENDPOINT_CATEGORIES]; + +// Pubky App Spec models +export const APP_SPEC_MODELS = { + USER: 'user', + POST: 'post', + TAG: 'tag', + BOOKMARK: 'bookmark', + FOLLOW: 'follow', + FILE: 'file', + FEED: 'feed', + MUTE: 'mute', + LAST_READ: 'last_read', + BLOB: 'blob', +} as const; + +export type AppSpecModel = (typeof APP_SPEC_MODELS)[keyof typeof APP_SPEC_MODELS]; + +// Pkarr concepts +export const PKARR_CONCEPTS = { + DISCOVERY: 'discovery', + DHT: 'dht', + RELAY: 'relay', + SIGNED_PACKET: 'signed-packet', + DNS_RECORDS: 'dns-records', + REPUBLISHING: 'republishing', + KEYPAIR: 'keypair', + MAINLINE: 'mainline', +} as const; + +export type PkarrConcept = (typeof PKARR_CONCEPTS)[keyof typeof PKARR_CONCEPTS]; + +// Pkarr example types +export const PKARR_EXAMPLE_TYPES = { + PUBLISH: 'publish', + RESOLVE: 'resolve', + HTTP_SERVE: 'http-serve', + HTTP_GET: 'http-get', +} as const; + +export type PkarrExampleType = (typeof PKARR_EXAMPLE_TYPES)[keyof typeof PKARR_EXAMPLE_TYPES]; + +// Default Pkarr relay URLs +export const DEFAULT_PKARR_RELAYS = [ + 'https://pkarr.pubky.app', + 'https://pkarr.pubky.org', +] as const; + +// Pkarr relay default port +export const PKARR_RELAY_DEFAULT_PORT = 6881; + +// Pkarr design document types +export const PKARR_DESIGN_DOCS = { + BASE: 'base', + RELAYS: 'relays', + ENDPOINTS: 'endpoints', + TLS: 'tls', + RESOLVERS: 'resolvers', +} as const; + +export type PkarrDesignDoc = (typeof PKARR_DESIGN_DOCS)[keyof typeof PKARR_DESIGN_DOCS]; + +// PKDNS constants (DNS resolver for Pkarr domains) +export const PKDNS_DEFAULT_PORT = 53; +export const PKDNS_DOH_PORT = 443; + +export const PKDNS_COMMANDS = { + PUBLISH: 'publish', + RESOLVE: 'resolve', + GENERATE: 'generate', + PUBLICKEY: 'publickey', +} as const; + +export type PkdnsCommand = (typeof PKDNS_COMMANDS)[keyof typeof PKDNS_COMMANDS]; + +export const PUBLIC_PKDNS_SERVERS = [ + 'https://dns.pubky.app', + 'https://dns.pubky.org', +] as const; + +// Pubky Nexus constants (social indexer implementation) +export const NEXUS_COMPONENTS = { + WATCHER: 'watcher', + SERVICE: 'service', + NEXUSD: 'nexusd', + COMMON: 'common', +} as const; + +export type NexusComponent = (typeof NEXUS_COMPONENTS)[keyof typeof NEXUS_COMPONENTS]; + +export const NEXUS_DATABASES = { + NEO4J: 'neo4j', + REDIS: 'redis', +} as const; + +export type NexusDatabase = (typeof NEXUS_DATABASES)[keyof typeof NEXUS_DATABASES]; diff --git a/pubky-mcp-server/src/index.ts b/pubky-mcp-server/src/index.ts new file mode 100644 index 0000000..9cd834e --- /dev/null +++ b/pubky-mcp-server/src/index.ts @@ -0,0 +1,194 @@ +#!/usr/bin/env node +/** + * Pubky MCP Server + * + * Model Context Protocol server that provides comprehensive Pubky protocol knowledge, + * code examples, and development tools for building Pubky applications. + */ + +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { + CallToolRequestSchema, + ListResourcesRequestSchema, + ListToolsRequestSchema, + ReadResourceRequestSchema, + ListPromptsRequestSchema, + GetPromptRequestSchema, +} from '@modelcontextprotocol/sdk/types.js'; + +import { FileReader } from './utils/file-reader.js'; +import { ResourceHandler } from './resources.js'; +import { ToolHandler } from './tools.js'; +import { PromptHandler } from './prompts.js'; +import * as path from 'path'; +import { fileURLToPath } from 'url'; + +// Get the directory of the current module (works in ESM) +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Point to bundled data directory (dist/../data after build) +const DATA_ROOT = path.join(__dirname, '..', 'data'); +const PUBKY_CORE_ROOT = path.join(DATA_ROOT, 'pubky-core'); +const PKARR_ROOT = path.join(DATA_ROOT, 'pkarr'); +const PKDNS_ROOT = path.join(DATA_ROOT, 'pkdns'); +const NEXUS_ROOT = path.join(DATA_ROOT, 'pubky-nexus'); +const WORKSPACE_ROOT = DATA_ROOT; + +// Initialize handlers +const fileReader = new FileReader(PUBKY_CORE_ROOT, PKARR_ROOT, PKDNS_ROOT, NEXUS_ROOT); +const resourceHandler = new ResourceHandler(fileReader, WORKSPACE_ROOT); +const toolHandler = new ToolHandler(fileReader, PUBKY_CORE_ROOT, WORKSPACE_ROOT, PKARR_ROOT, PKDNS_ROOT, NEXUS_ROOT); +const promptHandler = new PromptHandler(); + +// Create MCP server +const server = new Server( + { + name: 'pubky-mcp-server', + version: '1.0.0', + }, + { + capabilities: { + resources: {}, + tools: {}, + prompts: {}, + }, + } +); + +// Error handler +server.onerror = error => { + console.error('[MCP Error]', error); +}; + +// Handle process errors +process.on('SIGINT', async () => { + await server.close(); + process.exit(0); +}); + +process.on('SIGTERM', async () => { + await server.close(); + process.exit(0); +}); + +// List resources handler +server.setRequestHandler(ListResourcesRequestSchema, async () => { + try { + const resources = await resourceHandler.listResources(); + return { resources }; + } catch (error) { + console.error('Error listing resources:', error); + return { resources: [] }; + } +}); + +// Read resource handler +server.setRequestHandler(ReadResourceRequestSchema, async request => { + try { + const { uri } = request.params; + return await resourceHandler.getResource(uri); + } catch (error: any) { + console.error('Error reading resource:', error); + throw new Error(`Failed to read resource: ${error.message}`); + } +}); + +// List tools handler +server.setRequestHandler(ListToolsRequestSchema, async () => { + try { + const tools = toolHandler.listTools(); + return { tools }; + } catch (error) { + console.error('Error listing tools:', error); + return { tools: [] }; + } +}); + +// Call tool handler +server.setRequestHandler(CallToolRequestSchema, async request => { + try { + const { name, arguments: args } = request.params; + return await toolHandler.executeTool(name, args || {}); + } catch (error: any) { + console.error('Error executing tool:', error); + return { + content: [ + { + type: 'text', + text: `Error: ${error.message}`, + }, + ], + }; + } +}); + +// List prompts handler +server.setRequestHandler(ListPromptsRequestSchema, async () => { + try { + const prompts = promptHandler.listPrompts(); + return { prompts }; + } catch (error) { + console.error('Error listing prompts:', error); + return { prompts: [] }; + } +}); + +// Get prompt handler +server.setRequestHandler(GetPromptRequestSchema, async request => { + try { + const { name, arguments: args } = request.params; + return await promptHandler.getPrompt(name, args || {}); + } catch (error: any) { + console.error('Error getting prompt:', error); + throw new Error(`Failed to get prompt: ${error.message}`); + } +}); + +// Start the server +async function main() { + console.error('πŸš€ Pubky MCP Server starting...'); + console.error(`πŸ“‚ Data path: ${DATA_ROOT}`); + + // Verify bundled data exists + try { + await fileReader.fileExists(path.join(PUBKY_CORE_ROOT, 'README.md')); + console.error('βœ… Bundled resources found'); + } catch { + console.error(`⚠️ Warning: Bundled resources not found at ${DATA_ROOT}`); + console.error('Please run: npm run fetch-resources'); + } + + const transport = new StdioServerTransport(); + await server.connect(transport); + + console.error('βœ… Pubky MCP Server running'); + console.error(''); + console.error('πŸ“¦ Available capabilities:'); + console.error(' β€’ Resources: Documentation, API specs, code examples'); + console.error(' β€’ Tools: Code generation, validation, testnet management'); + console.error(' β€’ Prompts: Interactive guides for building on Pubky'); + console.error(''); + console.error('πŸ”Ί Complete Pubky Stack (4 Layers + Tools):'); + console.error(' β€’ Layer 1a: Pkarr (protocol) - Discovery via Mainline DHT'); + console.error(' β€’ Layer 1b: Pkdns (resolver) - DNS server for Pkarr domains'); + console.error(' β€’ Layer 2: Pubky Core (protocol) - HOW to read/write homeserver'); + console.error(' β€’ Layer 3: App Specs (models) - WHAT format to use'); + console.error(' β€’ Layer 4a: Nexus API (interface) - WHERE you READ social data'); + console.error(' β€’ Layer 4b: Nexus (implementation) - Graph indexer internals'); + console.error(''); + console.error('πŸ’‘ Complete Flow:'); + console.error(' 1. Resolve pubkey β†’ homeserver URL (Pkarr + Pkdns)'); + console.error(' 2. Write data to YOUR homeserver (Pubky Core + App Specs)'); + console.error(' 3. Nexus watcher indexes from all homeservers'); + console.error(' 4. Read aggregated social data (Nexus API)'); + console.error(''); + console.error('πŸ› οΈ Now covering 6 Pubky projects with 65+ tools!'); + console.error('Your complete Pubky ecosystem expert is ready! πŸŽ‰'); +} + +main().catch(error => { + console.error('Fatal error:', error); + process.exit(1); +}); diff --git a/pubky-mcp-server/src/prompts.ts b/pubky-mcp-server/src/prompts.ts new file mode 100644 index 0000000..890f850 --- /dev/null +++ b/pubky-mcp-server/src/prompts.ts @@ -0,0 +1,2792 @@ +/** + * MCP Prompts for common Pubky development tasks + * + * TABLE OF CONTENTS: + * ================== + * + * ARCHITECTURE & LEARNING: + * - understand-architecture: Explain hub-spoke architecture + * + * DEVELOPMENT WORKFLOW: + * - create-pubky-app: Interactive app creation + * - implement-auth: Authentication guide + * - add-storage: Storage operations guide + * - write-to-homeserver: Writing data guide + * - debug-capabilities: Debug permissions + * - setup-testnet: Local testnet setup + * + * SOCIAL FEATURES: + * - build-social-feed: Social feed implementation + * - read-from-nexus: Querying Nexus API + * - create-post-ui: Post creation UI + * - implement-user-profile: Profile management + * - query-social-data: Social data queries + * + * DATA & VALIDATION: + * - validate-app-data: Data validation guide + * + * NOTE: For better maintainability, consider splitting into: + * - prompts/architecture.ts + * - prompts/development.ts + * - prompts/social.ts + * - prompts/data.ts + */ + +import { Prompt } from '@modelcontextprotocol/sdk/types.js'; + +export class PromptHandler { + listPrompts(): Prompt[] { + return [ + { + name: 'understand-architecture', + description: 'Explain the Pubky hub-spoke architecture: where to write (homeserver) vs where to read (Nexus)', + arguments: [], + }, + { + name: 'create-pubky-app', + description: 'Interactive guide to create a new Pubky application from scratch', + arguments: [ + { + name: 'project_name', + description: 'Name of the project to create', + required: true, + }, + { + name: 'language', + description: 'Programming language (rust, javascript, typescript)', + required: false, + }, + { + name: 'features', + description: 'Comma-separated features (auth, storage, json)', + required: false, + }, + ], + }, + { + name: 'implement-auth', + description: 'Guide to implement Pubky authentication in your app', + arguments: [ + { + name: 'language', + description: 'Programming language of your project', + required: true, + }, + ], + }, + { + name: 'add-storage', + description: 'Guide to add storage operations to your Pubky app (writing to YOUR homeserver)', + arguments: [ + { + name: 'language', + description: 'Programming language of your project', + required: true, + }, + { + name: 'operation', + description: 'Storage operation (read, write, list, delete)', + required: false, + }, + ], + }, + { + name: 'write-to-homeserver', + description: 'Guide for writing data to your homeserver using pubky-app-specs format', + arguments: [ + { + name: 'data_type', + description: 'Type of data to write (post, profile, tag, bookmark)', + required: true, + }, + { + name: 'language', + description: 'Programming language (javascript, typescript, rust)', + required: false, + }, + ], + }, + { + name: 'debug-capabilities', + description: 'Help debug and understand capability/permission issues', + arguments: [ + { + name: 'error_message', + description: "The error message you're seeing", + required: false, + }, + { + name: 'capabilities', + description: "The capabilities you're using", + required: false, + }, + ], + }, + { + name: 'setup-testnet', + description: 'Guide to set up and use local Pubky testnet for development', + arguments: [], + }, + { + name: 'build-social-feed', + description: 'Guide to build a social feed by reading from Nexus API (aggregated social data)', + arguments: [ + { + name: 'language', + description: 'Programming language (javascript, typescript, rust)', + required: true, + }, + { + name: 'feed_type', + description: 'Type of feed (following, all, bookmarks)', + required: false, + }, + ], + }, + { + name: 'read-from-nexus', + description: 'Guide for reading social data from Nexus indexer (feeds, search, discovery)', + arguments: [ + { + name: 'query_type', + description: 'Type of query (feed, search, user-lookup, post-details)', + required: true, + }, + { + name: 'language', + description: 'Programming language (javascript, typescript, rust)', + required: false, + }, + ], + }, + { + name: 'create-post-ui', + description: 'Guide for creating posts with validation and writing to homeserver', + arguments: [ + { + name: 'language', + description: 'Programming language (javascript, typescript)', + required: true, + }, + { + name: 'post_type', + description: 'Type of post (short, long, image, video)', + required: false, + }, + ], + }, + { + name: 'implement-user-profile', + description: 'Guide for user profile management (writing to homeserver, reading from Nexus)', + arguments: [ + { + name: 'language', + description: 'Programming language (javascript, typescript, rust)', + required: true, + }, + ], + }, + { + name: 'query-social-data', + description: 'Guide for querying aggregated social data from Nexus indexer', + arguments: [ + { + name: 'data_type', + description: 'Type of data (posts, users, tags, streams)', + required: true, + }, + { + name: 'language', + description: 'Programming language (javascript, typescript, rust)', + required: false, + }, + ], + }, + { + name: 'validate-app-data', + description: 'Guide for validating data using pubky-app-specs before writing to homeserver', + arguments: [ + { + name: 'model', + description: 'Model to validate (user, post, tag, bookmark, follow)', + required: true, + }, + ], + }, + ]; + } + + async getPrompt( + name: string, + args: Record + ): Promise<{ messages: Array<{ role: string; content: { type: string; text: string } }> }> { + switch (name) { + case 'understand-architecture': + return this.understandArchitecturePrompt(); + case 'create-pubky-app': + return this.createPubkyAppPrompt(args); + case 'implement-auth': + return this.implementAuthPrompt(args); + case 'add-storage': + return this.addStoragePrompt(args); + case 'write-to-homeserver': + return this.writeToHomeserverPrompt(args); + case 'debug-capabilities': + return this.debugCapabilitiesPrompt(args); + case 'setup-testnet': + return this.setupTestnetPrompt(); + case 'build-social-feed': + return this.buildSocialFeedPrompt(args); + case 'read-from-nexus': + return this.readFromNexusPrompt(args); + case 'create-post-ui': + return this.createPostUiPrompt(args); + case 'implement-user-profile': + return this.implementUserProfilePrompt(args); + case 'query-social-data': + return this.querySocialDataPrompt(args); + case 'validate-app-data': + return this.validateAppDataPrompt(args); + default: + throw new Error(`Unknown prompt: ${name}`); + } + } + + private async understandArchitecturePrompt(): Promise<{ + messages: Array<{ role: string; content: { type: string; text: string } }>; + }> { + const prompt = `# Understanding Pubky Architecture + +Pubky uses a **hub-spoke architecture** where your app interacts with different components for different purposes. + +## The Architecture + +\`\`\` + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ NEXUS β”‚ ← Crawls/indexes public data + β”‚ (Indexer) β”‚ from all homeservers (~0.5s) + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β–² + β”‚ GET (read social data) + β”‚ + β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β” + β”‚ Your β”‚ + β”‚ App β”‚ + β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ + β”‚ + β”‚ PUT/DELETE (write your data) + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Your Homeserver β”‚ ← Your personal storage + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +\`\`\` + +## Three Components + +### 1. Your Homeserver (pubky-core) +**WHERE you WRITE data** + +- Your personal storage backend +- You authenticate here +- PUT/DELETE operations to store your data +- Example: \`PUT /pub/pubky.app/posts/{id}\` + +### 2. Data Specs (pubky-app-specs) +**WHAT format to use** + +- Standardized data models (User, Post, Tag, etc.) +- Validation rules for interoperability +- Everyone follows the same format +- Example: PubkyAppPost has max 2000 chars for short posts + +### 3. Nexus Indexer (nexus-webapi) +**WHERE you READ social data** + +- Aggregates data from ALL homeservers +- Crawls and indexes every ~0.5 seconds +- Fast queries for feeds, search, discovery +- Example: \`GET /v0/stream/following\` + +## Data Flow + +### Writing Data +1. User creates a post in your app +2. Format it using \`pubky-app-specs\` (PubkyAppPost) +3. \`PUT\` it to YOUR homeserver +4. Nexus automatically discovers and indexes it +5. Other users can now see it via Nexus + +### Reading Social Data +1. User wants to see their feed +2. Your app queries Nexus API +3. Nexus returns aggregated, indexed data +4. Fast and efficient (no need to query 100+ homeservers) + +## Key Insights + +βœ… **Write to YOUR homeserver** - Direct, authenticated storage +βœ… **Read from NEXUS** - Fast, aggregated social queries +βœ… **Use pubky-app-specs** - Ensures interoperability +❌ **Don't query other homeservers directly** - Too slow for social features + +## Example Flow: Creating & Viewing a Post + +\`\`\`javascript +// 1. Write to YOUR homeserver +const post = new PubkyAppPost("Hello world!", "short"); +await client.put(\`/pub/pubky.app/posts/\${postId}\`, post.toJson()); +// Nexus will automatically index this within ~0.5 seconds + +// 2. Read from NEXUS for social features +const feed = await nexusClient.get('/v0/stream/following?viewer_id=...'); +// Returns posts from all users you follow (aggregated & indexed) +\`\`\` + +## Next Steps + +- Use \`write-to-homeserver\` prompt to learn about writing data +- Use \`read-from-nexus\` prompt to learn about querying Nexus +- Use \`create-post-ui\` to see a full example + +Would you like to explore any specific component in detail?`; + + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: prompt, + }, + }, + ], + }; + } + + private async createPubkyAppPrompt( + args: Record + ): Promise<{ messages: Array<{ role: string; content: { type: string; text: string } }> }> { + const projectName = args.project_name || 'my-pubky-app'; + const language = args.language || 'javascript'; + const features = args.features + ? args.features.split(',').map(f => f.trim()) + : ['auth', 'storage']; + + const prompt = `I'll help you create a new Pubky application called "${projectName}" in ${language}. + +## Steps to Create Your Pubky App + +### 1. Project Setup + +First, let's generate the project scaffold: + +Use the \`generate_app_scaffold\` tool with: +- project_name: ${projectName} +- language: ${language} +- features: ${JSON.stringify(features)} + +### 2. Install Dependencies + +${ + language === 'rust' + ? ` +After the scaffold is created: +\`\`\`bash +cd ${projectName} +cargo build +\`\`\` +` + : ` +After the scaffold is created: +\`\`\`bash +cd ${projectName} +npm install +\`\`\` +` +} + +### 3. Set Up Local Testnet (Recommended) + +For local development, start a testnet: + +Use the \`start_testnet\` tool, or manually: +\`\`\`bash +${language === 'rust' ? 'cargo install pubky-testnet && pubky-testnet' : 'npx pubky-testnet'} +\`\`\` + +The testnet homeserver public key is: \`8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo\` + +### 4. Update Your Code + +${ + language === 'rust' + ? ` +In \`src/main.rs\`, change: +\`\`\`rust +let pubky = Pubky::testnet()?; // For local development +// let pubky = Pubky::new()?; // For production +\`\`\` +` + : ` +In your main file, change: +\`\`\`javascript +const pubky = Pubky.testnet(); // For local development +// const pubky = new Pubky(); // For production +\`\`\` +` +} + +### 5. Run Your App + +\`\`\`bash +${language === 'rust' ? 'cargo run' : 'npm start'} +\`\`\` + +## What Features Are Included? + +${ + features.includes('auth') + ? ` +### Authentication +Your app includes Pubky Auth flow for keyless authentication. Users can scan a QR code with their Pubky authenticator (like Pubky Ring app). +` + : '' +} + +${ + features.includes('storage') + ? ` +### Storage +Your app can read and write to the user's Pubky homeserver storage. All storage operations are under \`/pub/my-app/\` by default. +` + : '' +} + +${ + features.includes('json') + ? ` +### JSON Support +Your app includes JSON serialization helpers for easy data handling. +` + : '' +} + +## Next Steps + +1. Explore the generated code +2. Read the README.md in your project +3. Check out more examples with \`get_code_example\` +4. Learn about capabilities with \`explain_capabilities\` + +Need help with anything? Just ask!`; + + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `Create a new Pubky app called "${projectName}" in ${language}`, + }, + }, + { + role: 'assistant', + content: { + type: 'text', + text: prompt, + }, + }, + ], + }; + } + + private async implementAuthPrompt( + args: Record + ): Promise<{ messages: Array<{ role: string; content: { type: string; text: string } }> }> { + const language = args.language || 'javascript'; + + const prompt = `I'll help you implement Pubky authentication in your ${language} app. + +## Pubky Authentication Flow + +Pubky uses a QR code-based authentication flow that allows users to sign in with their Pubky identity (like Pubky Ring app) without exposing their keys to your app. + +### How It Works + +1. **Your App**: Generates an authorization URL with required capabilities +2. **User**: Scans the URL (QR code) with their Pubky authenticator +3. **Authenticator**: Shows what permissions your app is requesting +4. **User**: Approves or denies +5. **Your App**: Receives a session with the approved capabilities + +### Implementation + +${ + language === 'rust' + ? ` +\`\`\`rust +use pubky::prelude::*; + +#[tokio::main] +async fn main() -> pubky::Result<()> { + let pubky = Pubky::testnet()?; // or Pubky::new() for production + + // Define what your app needs access to + let caps = Capabilities::builder() + .read_write("/pub/my-app/") // Full access to your app's directory + .read("/pub/pubky.app/profile") // Read-only access to user's profile + .finish(); + + // Start the auth flow + let flow = pubky.start_auth_flow(&caps)?; + + println!("Scan this URL:"); + println!("{}", flow.authorization_url()); + + // In a web app, you'd show this as a QR code + // For testing, you can copy-paste to the authenticator + + // Wait for user approval (this will block until user approves/denies) + let session = flow.await_approval().await?; + + println!("βœ… Authenticated!"); + println!("User: {}", session.info().public_key()); + + // Now you can use the session to access storage + session.storage() + .put("/pub/my-app/data.json", r#"{"welcome": true}"#) + .await?; + + Ok(()) +} +\`\`\` +` + : ` +\`\`\`javascript +import { Pubky, Capabilities } from '@synonymdev/pubky'; + +const pubky = Pubky.testnet(); // or new Pubky() for production + +// Define what your app needs access to +const caps = Capabilities.builder() + .readWrite('/pub/my-app/') // Full access to your app's directory + .read('/pub/pubky.app/profile') // Read-only access to user's profile + .finish(); + +// Start the auth flow +const flow = pubky.startAuthFlow(caps); + +// Show the authorization URL as a QR code +console.log('Scan this QR code:'); +console.log(flow.authorizationUrl()); + +// In a web app, you might use a QR code library: +// import QRCode from 'qrcode'; +// QRCode.toDataURL(flow.authorizationUrl()); + +// Wait for user approval +const session = await flow.awaitApproval(); + +console.log('βœ… Authenticated!'); +console.log('User:', session.publicKey()); + +// Now you can use the session +await session.storage().put('/pub/my-app/data.json', JSON.stringify({ welcome: true })); +\`\`\` +` +} + +### Key Points + +1. **Capabilities**: Be specific about what you need. Don't request \`/:rw\` (everything) unless absolutely necessary. + +2. **QR Codes**: In a web app, convert the authorization URL to a QR code for easy scanning. + +3. **Session Management**: Store the session for subsequent requests. ${language === 'rust' ? 'You can persist it with `session.write_secret_file()`.' : 'You can save it to a file or database.'} + +4. **Testnet**: For development, use Pubky.testnet() and start a local testnet with \`start_testnet\` tool. + +### Testing Authentication + +1. Start your local testnet (if not already running) +2. Run your app +3. Use an authenticator app (Pubky Ring) or the CLI authenticator +4. Scan/paste the auth URL +5. Approve the capabilities +6. Your app receives the session! + +### Common Issues + +- **Timeout**: Make sure your HTTP relay is accessible +- **No Approval**: Check that the authenticator has the correct homeserver configured +- **401 Errors**: Verify the session is being sent with requests + +Need help with a specific part? Just ask!`; + + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `How do I implement Pubky authentication in my ${language} app?`, + }, + }, + { + role: 'assistant', + content: { + type: 'text', + text: prompt, + }, + }, + ], + }; + } + + private async addStoragePrompt( + args: Record + ): Promise<{ messages: Array<{ role: string; content: { type: string; text: string } }> }> { + const language = args.language || 'javascript'; + const operation = args.operation || 'all'; + + const prompt = `I'll help you add storage operations to your Pubky app in ${language}. + +## Pubky Storage Overview + +Pubky provides a simple key-value storage API via HTTP. There are two types of storage access: + +1. **Session Storage**: Authenticated access to your own or authorized storage +2. **Public Storage**: Read-only access to anyone's public data + +### Path Conventions + +- \`/pub/*\` - Public data (readable by anyone) +- \`/pub/my-app/*\` - Your app's public data +- Other paths may be restricted or private + +${ + operation === 'write' || operation === 'all' + ? ` +### Writing Data + +${ + language === 'rust' + ? ` +\`\`\`rust +use pubky::prelude::*; + +async fn write_data(session: &PubkySession) -> pubky::Result<()> { + // Write text + session.storage() + .put("/pub/my-app/hello.txt", "Hello, World!") + .await?; + + // Write JSON (with feature "json") + #[cfg(feature = "json")] + { + use serde::{Serialize, Deserialize}; + + #[derive(Serialize)] + struct Profile { + name: String, + bio: String, + } + + let profile = Profile { + name: "Alice".to_string(), + bio: "Pubky developer".to_string(), + }; + + session.storage() + .put_json("/pub/my-app/profile.json", &profile) + .await?; + } + + // Write binary data + let image_bytes = std::fs::read("avatar.png")?; + session.storage() + .put("/pub/my-app/avatar.png", image_bytes) + .await?; + + Ok(()) +} +\`\`\` +` + : ` +\`\`\`javascript +async function writeData(session) { + // Write text + await session.storage().put('/pub/my-app/hello.txt', 'Hello, World!'); + + // Write JSON + const profile = { + name: 'Alice', + bio: 'Pubky developer' + }; + await session.storage().put('/pub/my-app/profile.json', JSON.stringify(profile)); + + // Write binary data (in browser) + const file = document.getElementById('fileInput').files[0]; + const arrayBuffer = await file.arrayBuffer(); + await session.storage().put('/pub/my-app/avatar.png', arrayBuffer); +} +\`\`\` +` +} +` + : '' +} + +${ + operation === 'read' || operation === 'all' + ? ` +### Reading Data (Authenticated) + +${ + language === 'rust' + ? ` +\`\`\`rust +async fn read_data(session: &PubkySession) -> pubky::Result<()> { + // Read text + let response = session.storage() + .get("/pub/my-app/hello.txt") + .await?; + let text = response.text().await?; + println!("Content: {}", text); + + // Read JSON (with feature "json") + #[cfg(feature = "json")] + { + let profile: Profile = session.storage() + .get_json("/pub/my-app/profile.json") + .await?; + println!("Name: {}", profile.name); + } + + // Read binary + let response = session.storage() + .get("/pub/my-app/avatar.png") + .await?; + let bytes = response.bytes().await?; + + Ok(()) +} +\`\`\` +` + : ` +\`\`\`javascript +async function readData(session) { + // Read text + const response = await session.storage().get('/pub/my-app/hello.txt'); + const text = await response.text(); + console.log('Content:', text); + + // Read JSON + const profileResponse = await session.storage().get('/pub/my-app/profile.json'); + const profile = JSON.parse(await profileResponse.text()); + console.log('Name:', profile.name); + + // Read binary + const imageResponse = await session.storage().get('/pub/my-app/avatar.png'); + const blob = await imageResponse.blob(); + // Use blob in browser, e.g., create object URL for + const url = URL.createObjectURL(blob); +} +\`\`\` +` +} + +### Reading Public Data (No Authentication Required) + +${ + language === 'rust' + ? ` +\`\`\`rust +async fn read_public_data(pubky: &Pubky, user_pk: &PublicKey) -> pubky::Result<()> { + let public_storage = pubky.public_storage(); + + // Read a file + let response = public_storage + .get(format!("pubky{}/pub/my-app/profile.json", user_pk)) + .await?; + let text = response.text().await?; + + // List files in a directory + let entries = public_storage + .list(format!("pubky{}/pub/my-app/", user_pk))? + .limit(10) + .send() + .await?; + + for entry in entries { + println!("- {}", entry.path); + } + + Ok(()) +} +\`\`\` +` + : ` +\`\`\`javascript +async function readPublicData(pubky, userPublicKey) { + const publicStorage = pubky.publicStorage(); + + // Read a file + const response = await publicStorage.get(\`pubky\${userPublicKey}/pub/my-app/profile.json\`); + const profile = JSON.parse(await response.text()); + + // List files in a directory + const entries = await publicStorage + .list(\`pubky\${userPublicKey}/pub/my-app/\`) + .limit(10) + .send(); + + for (const entry of entries) { + console.log('-', entry.path); + } +} +\`\`\` +` +} +` + : '' +} + +${ + operation === 'list' || operation === 'all' + ? ` +### Listing Files + +${ + language === 'rust' + ? ` +\`\`\`rust +// List with session +let entries = session.storage() + .list("/pub/my-app/")? + .limit(20) + .send() + .await?; + +for entry in entries { + println!("{} - {} bytes", entry.path, entry.size); +} +\`\`\` +` + : ` +\`\`\`javascript +// List with session +const entries = await session.storage() + .list('/pub/my-app/') + .limit(20) + .send(); + +for (const entry of entries) { + console.log(\`\${entry.path} - \${entry.size} bytes\`); +} +\`\`\` +` +} +` + : '' +} + +${ + operation === 'delete' || operation === 'all' + ? ` +### Deleting Files + +${ + language === 'rust' + ? ` +\`\`\`rust +session.storage() + .delete("/pub/my-app/old-data.json") + .await?; +\`\`\` +` + : ` +\`\`\`javascript +await session.storage().delete('/pub/my-app/old-data.json'); +\`\`\` +` +} +` + : '' +} + +### Best Practices + +1. **Organize Your Data**: Use a consistent directory structure like \`/pub/my-app/users/{userId}/...\` + +2. **Handle Errors**: Always handle cases where files don't exist (404) or operations fail + +3. **Size Limits**: Files are limited to 100MB by default on homeservers + +4. **Capabilities**: Make sure your session has the right capabilities (\`read\` for GET, \`write\` for PUT/DELETE) + +5. **Caching**: Consider caching frequently accessed data + +Need more help? Use \`get_code_example\` for full examples!`; + + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `How do I add storage operations to my Pubky app?`, + }, + }, + { + role: 'assistant', + content: { + type: 'text', + text: prompt, + }, + }, + ], + }; + } + + private async writeToHomeserverPrompt( + args: Record + ): Promise<{ messages: Array<{ role: string; content: { type: string; text: string } }> }> { + const dataType = args.data_type || 'post'; + const language = args.language || 'javascript'; + + const prompt = `# Writing to Your Homeserver + +I'll guide you through writing ${dataType} data to YOUR homeserver using the pubky-app-specs format. + +## Key Concept + +When you write data to your homeserver: +1. Format it using **pubky-app-specs** (ensures interoperability) +2. **PUT** it to YOUR homeserver with proper authentication +3. Nexus will automatically discover and index it within ~0.5 seconds +4. Other users can then read it via Nexus API + +## Example: Writing a ${dataType.charAt(0).toUpperCase() + dataType.slice(1)} + +Use the \`generate_data_model\` tool to get the proper format: +- model: ${dataType} +- language: ${language} + +Then use \`create_model_example\` to see a complete working example. + +## General Pattern + +\`\`\`${language === 'rust' ? 'rust' : 'javascript'} +${language === 'rust' ? ` +// 1. Create and validate data +let data = PubkyApp${dataType.charAt(0).toUpperCase() + dataType.slice(1)}::new(...); +data.validate()?; + +// 2. Create path +let id = data.create_id(); +let path = PubkyApp${dataType.charAt(0).toUpperCase() + dataType.slice(1)}::create_path(&id); + +// 3. Authenticate and write +let client = Pubky::builder() + .homeserver(homeserver_url) + .build()?; +client.signin(capabilities).await?; +client.put(&path, &serde_json::to_vec(&data)?).await?; +` : ` +// 1. Create and validate data +const builder = new PubkySpecsBuilder(userId); +const { ${dataType}, meta } = builder.create${dataType.charAt(0).toUpperCase() + dataType.slice(1)}(...); + +// 2. Authenticate +const client = new Pubky(); +await client.signup(capabilities); + +// 3. Write to homeserver +await client.put(meta.url, ${dataType}.toJson()); +`} +\`\`\` + +## Important Notes + +βœ… **Write to YOUR homeserver** - Not to Nexus! +βœ… **Use pubky-app-specs format** - For interoperability +βœ… **Store in /pub/pubky.app/** - So Nexus can index it +βœ… **Authenticate first** - Need valid session +❌ **Don't write to Nexus** - It only reads/indexes + +## Next Steps + +1. Use \`generate_data_model\` to get the proper ${dataType} format +2. Use \`create_model_example\` to see a full example +3. Use \`implement-auth\` if you need help with authentication +4. Use \`add-storage\` for more storage operation examples + +Want me to show you a complete example with authentication?`; + + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `Guide me through writing ${dataType} data to my homeserver in ${language}`, + }, + }, + { + role: 'assistant', + content: { + type: 'text', + text: prompt, + }, + }, + ], + }; + } + + private async debugCapabilitiesPrompt( + args: Record + ): Promise<{ messages: Array<{ role: string; content: { type: string; text: string } }> }> { + const errorMessage = args.error_message || ''; + const capabilities = args.capabilities || ''; + + const prompt = `I'll help you debug capability/permission issues in your Pubky app. + +${errorMessage ? `## Error Analysis\n\nYou're seeing: "${errorMessage}"\n\n` : ''} +${capabilities ? `## Your Capabilities\n\nYou're using: \`${capabilities}\`\n\n` : ''} + +## Common Capability Issues + +### 1. 401 Unauthorized +**Problem**: No session or invalid session cookie +**Solution**: +- Make sure you've called \`signin()\` or \`signup()\` first +- Verify the session cookie is being sent with requests +- Check that the session hasn't expired + +### 2. 403 Forbidden +**Problem**: Session exists but lacks required capabilities +**Solution**: +- Review your capabilities - do they cover the path you're accessing? +- Example: If you're writing to \`/pub/my-app/data.json\`, you need \`/pub/my-app/:w\` or \`/:w\` +- Use \`explain_capabilities\` tool to understand what your capabilities grant + +### 3. Capability Scope Rules + +**Exact Match**: \`/pub/my-app/file.txt:r\` - Only this specific file +**Directory Match**: \`/pub/my-app/:r\` - All files under /pub/my-app/ (note the trailing /) +**Root Match**: \`/:rw\` - Everything (use cautiously!) + +### 4. Read vs Write + +- \`:r\` - Read only (GET, HEAD) +- \`:w\` - Write only (PUT, POST, DELETE) +- \`:rw\` - Both read and write + +### 5. Capability Best Practices + +βœ… **DO**: +- Be specific: \`/pub/my-app/:rw\` instead of \`/:rw\` +- Request minimum needed permissions +- Use read-only (\`:r\`) when possible + +❌ **DON'T**: +- Request \`/:rw\` unless you really need everything +- Mix path specificity unnecessarily +- Forget the trailing \`/\` for directory access + +## Debugging Steps + +1. **Check your capabilities**: + Use \`explain_capabilities\` tool with your capability string + +2. **Verify the path**: + Does your capability scope cover the path you're accessing? + - Accessing: \`/pub/my-app/data/file.json\` + - Capability: \`/pub/my-app/:rw\` βœ… + - Capability: \`/pub/other-app/:rw\` ❌ + +3. **Check the action**: + - Reading? Need \`:r\` or \`:rw\` + - Writing/Deleting? Need \`:w\` or \`:rw\` + +4. **Test with broader permissions**: + Try \`/:rw\` temporarily to see if it's a capability issue + If it works, narrow down to the specific scope you need + +5. **Check session validity**: + Make sure you're authenticated and the session is active + +## Example Fix + +**Problem**: Getting 403 when writing to \`/pub/my-app/posts/123.json\` + +**Original Capability**: \`/pub/my-app:rw\` (no trailing slash!) + +**Fix**: \`/pub/my-app/:rw\` (with trailing slash for directory matching) + +Or more specific: \`/pub/my-app/posts/:rw\` + +${capabilities ? `\n## Analysis of Your Capabilities\n\nUse the \`explain_capabilities\` tool with your capability string to get detailed information.\n` : ''} + +Need more help? Share your code and I can take a closer look!`; + + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `Help me debug capability issues${errorMessage ? `: ${errorMessage}` : ''}`, + }, + }, + { + role: 'assistant', + content: { + type: 'text', + text: prompt, + }, + }, + ], + }; + } + + private async setupTestnetPrompt(): Promise<{ + messages: Array<{ role: string; content: { type: string; text: string } }>; + }> { + const prompt = `I'll help you set up a local Pubky testnet for development. + +## Why Use a Local Testnet? + +- **Offline Development**: No internet required +- **Fast Iteration**: Instant resets, no rate limits +- **Free**: No costs for storage or operations +- **Safe Testing**: Experiment without affecting production data + +## Setup Steps + +### 1. Install pubky-testnet + +**Option A: Using Cargo** (Recommended) +\`\`\`bash +cargo install pubky-testnet +\`\`\` + +**Option B: From Source** +\`\`\`bash +cd /path/to/pubky-core +cargo build -p pubky-testnet --release +\`\`\` + +Or use the MCP tool: +\`\`\` +Use the \`install_pubky_testnet\` tool +\`\`\` + +### 2. Start the Testnet + +**If installed via cargo:** +\`\`\`bash +pubky-testnet +\`\`\` + +**Or use the MCP tool:** +\`\`\` +Use the \`start_testnet\` tool +\`\`\` + +### 3. Testnet Information + +Once running, you'll have: + +- **Homeserver Public Key**: \`8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo\` +- **DHT Port**: 6881 +- **Pkarr Relay Port**: 15411 +- **HTTP Relay Port**: 15412 +- **Admin Server Port**: 6288 + +**URLs**: +- Homeserver: http://localhost:15411 +- HTTP Relay: http://localhost:15412 +- Admin: http://localhost:6288 + +### 4. Update Your Code + +**Rust**: +\`\`\`rust +// Instead of: +// let pubky = Pubky::new()?; + +// Use: +let pubky = Pubky::testnet()?; +\`\`\` + +**JavaScript**: +\`\`\`javascript +// Instead of: +// const pubky = new Pubky(); + +// Use: +const pubky = Pubky.testnet(); +\`\`\` + +### 5. Test It! + +Run your app and it will connect to your local testnet instead of production servers. + +## Managing the Testnet + +**Check Status**: +\`\`\` +Use \`check_testnet_status\` tool +\`\`\` + +**Get Info**: +\`\`\` +Use \`get_testnet_info\` tool +\`\`\` + +**Stop**: +\`\`\` +Use \`stop_testnet\` tool +\`\`\` +Or press Ctrl+C if running in terminal + +**Restart**: +\`\`\` +Use \`restart_testnet\` tool +\`\`\` + +## Testnet Features + +1. **Ephemeral**: Data is lost when stopped (perfect for testing!) +2. **No Signup Tokens**: Signup is open, no invitation needed +3. **Hardcoded Keys**: Same homeserver key every time +4. **Local Only**: Not accessible from outside your machine + +## Common Use Cases + +### Development Workflow +1. Start testnet at beginning of day +2. Develop and test your app +3. Stop testnet when done +4. Next day: start fresh! + +### CI/CD Testing +\`\`\`bash +# In your CI script +pubky-testnet & +TESTNET_PID=$! + +# Run your tests +npm test + +# Clean up +kill $TESTNET_PID +\`\`\` + +### Multiple Projects +You can run one testnet and use it for multiple projects simultaneously! + +## Troubleshooting + +**Port Already in Use**: +Something else is running on the required ports. Stop other services or change ports. + +**Can't Connect**: +Make sure testnet is running (\`check_testnet_status\` tool) and you're using \`Pubky.testnet()\`. + +**Data Disappeared**: +That's expected! Testnet is ephemeral. For persistent data, use a real homeserver. + +Ready to develop? Start your testnet and begin building!`; + + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: 'How do I set up a local Pubky testnet?', + }, + }, + { + role: 'assistant', + content: { + type: 'text', + text: prompt, + }, + }, + ], + }; + } + + private async buildSocialFeedPrompt( + args: Record + ): Promise<{ messages: Array<{ role: string; content: { type: string; text: string } }> }> { + const language = args.language || 'javascript'; + const feedType = args.feed_type || 'following'; + + const prompt = `I'll help you build a social feed using the Nexus API in ${language}. + +## Understanding Social Feeds + +The Nexus API provides powerful endpoints for building social feeds with Web-of-Trust filtering: + +- **All Posts**: Global timeline of all posts +- **Following**: Posts from users you follow +- **Followers**: Posts from your followers +- **Friends**: Posts from mutual follows +- **Bookmarks**: Your saved posts + +## Implementation Steps + +### 1. Set Up Nexus Client + +${ + language === 'javascript' || language === 'typescript' + ? `\`\`\`${language} +const NEXUS_API_URL = 'https://nexus.example.com'; + +async function fetchFeed(source = '${feedType}', observerId, options = {}) { + const params = new URLSearchParams({ + source, + observer_id: observerId, + limit: options.limit || 20, + skip: options.skip || 0, + sorting: options.sorting || 'timeline', + ...options + }); + + const response = await fetch(\`\${NEXUS_API_URL}/v0/stream/posts?\${params}\`); + if (!response.ok) throw new Error(\`HTTP error! status: \${response.status}\`); + + return await response.json(); +} +\`\`\` +` + : `\`\`\`rust +use reqwest; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize)] +struct PostView { + details: PostDetails, + counts: PostCounts, + tags: Vec, + // ... other fields +} + +async fn fetch_feed( + source: &str, + observer_id: &str, + limit: u32, +) -> Result, reqwest::Error> { + let url = format!( + "https://nexus.example.com/v0/stream/posts?source={}&observer_id={}&limit={}", + source, observer_id, limit + ); + + reqwest::get(&url).await?.json().await +} +\`\`\` +` +} + +### 2. Fetch and Display Posts + +${ + language === 'javascript' || language === 'typescript' + ? `\`\`\`${language} +// Fetch posts for the feed +const posts = await fetchFeed('${feedType}', userId, { + limit: 20, + sorting: 'timeline', // or 'total_engagement' + kind: 'short', // optional: filter by post type +}); + +// Display posts +posts.forEach(post => { + console.log(\`Post by \${post.details.author}:\`); + console.log(post.details.content); + console.log(\`❀️ \${post.counts.tags} | πŸ’¬ \${post.counts.replies} | πŸ”„ \${post.counts.reposts}\`); + console.log('---'); +}); +\`\`\` +` + : `\`\`\`rust +let posts = fetch_feed("${feedType}", &user_id, 20).await?; + +for post in posts { + println!("Post by {}", post.details.author); + println!("{}", post.details.content); + println!("❀️ {} | πŸ’¬ {} | πŸ”„ {}", + post.counts.tags, + post.counts.replies, + post.counts.reposts + ); + println!("---"); +} +\`\`\` +` +} + +### 3. Implement Pagination + +${ + language === 'javascript' || language === 'typescript' + ? `\`\`\`${language} +let skip = 0; +const limit = 20; + +async function loadMore() { + const morePosts = await fetchFeed('${feedType}', userId, { skip, limit }); + skip += limit; + return morePosts; +} +\`\`\` +` + : `\`\`\`rust +let mut skip = 0; +let limit = 20; + +async fn load_more(observer_id: &str, skip: &mut u32) -> Result, reqwest::Error> { + let posts = fetch_feed_with_pagination("${feedType}", observer_id, *skip, limit).await?; + *skip += limit; + Ok(posts) +} +\`\`\` +` +} + +### 4. Add Real-time Updates (Optional) + +Use the Nexus event stream to get live updates: + +\`\`\`${language === 'rust' ? 'rust' : 'javascript'} +${ + language === 'rust' + ? `// WebSocket or polling implementation for real-time updates +// Check Nexus API docs for SSE endpoint` + : `const eventSource = new EventSource(\`\${NEXUS_API_URL}/v0/events/?cursor=\${lastTimestamp}\`); + +eventSource.onmessage = (event) => { + const newPost = JSON.parse(event.data); + // Add to feed if it matches your criteria + prependPost(newPost); +};` +} +\`\`\` + +## Advanced Features + +### Filter by Tags + +\`\`\`${language === 'rust' ? 'rust' : 'javascript'} +${ + language === 'rust' + ? `let posts = fetch_feed_with_tags("following", &user_id, &["bitcoin", "nostr"]).await?;` + : `const posts = await fetchFeed('following', userId, { + tags: 'bitcoin,nostr' +});` +} +\`\`\` + +### Filter by Post Kind + +\`\`\`${language === 'rust' ? 'rust' : 'javascript'} +${ + language === 'rust' + ? `// Filter for only image posts +let posts = fetch_feed_by_kind("following", &user_id, "image").await?;` + : `// Filter for only video posts +const posts = await fetchFeed('following', userId, { + kind: 'video' +});` +} +\`\`\` + +### Time-based Queries + +\`\`\`${language === 'rust' ? 'rust' : 'javascript'} +${ + language === 'rust' + ? `// Get posts from last 24 hours +let day_ago = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() - 86400; +let posts = fetch_feed_since("all", day_ago).await?;` + : `// Get posts from last 24 hours +const dayAgo = Date.now() - 86400000; +const posts = await fetchFeed('all', userId, { + end: dayAgo +});` +} +\`\`\` + +## Best Practices + +1. **Pagination**: Always implement pagination for large feeds +2. **Caching**: Cache post data locally to reduce API calls +3. **Error Handling**: Handle 404 (no posts) and 500 (server error) gracefully +4. **Loading States**: Show loading indicators while fetching +5. **Web-of-Trust**: Use \`observer_id\` to get personalized feeds + +## Next Steps + +- Use \`query_nexus_api\` tool to explore more endpoints +- Use \`explain_nexus_endpoint\` for detailed API documentation +- Check \`pubky://api/nexus/schemas\` for complete data structures + +Need help with a specific part? Just ask!`; + + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `How do I build a ${feedType} feed in ${language}?`, + }, + }, + { + role: 'assistant', + content: { + type: 'text', + text: prompt, + }, + }, + ], + }; + } + + private async readFromNexusPrompt( + args: Record + ): Promise<{ messages: Array<{ role: string; content: { type: string; text: string } }> }> { + const queryType = args.query_type || 'feed'; + const language = args.language || 'javascript'; + + const prompt = `# Reading from Nexus Indexer + +I'll guide you through querying social data from the Nexus indexer. + +## Key Concept + +Nexus is an **indexer** that: +- Crawls ALL homeservers every ~0.5 seconds +- Indexes public data (stored in /pub/pubky.app/) +- Provides fast, aggregated queries for social features +- You READ from Nexus, never WRITE to it + +## Why Use Nexus? + +**Without Nexus:** To show a feed, you'd need to: +- Query homeserver 1 for User A's posts +- Query homeserver 2 for User B's posts +- Query homeserver 3 for User C's posts +- ... 100+ HTTP requests! 🐌 + +**With Nexus:** Single query β†’ instant results ⚑ + +## Query Type: ${queryType.toUpperCase()} + +Use the \`query_nexus_api\` tool to find relevant endpoints: +- query: "${queryType}" + +Then use \`explain_nexus_endpoint\` for detailed examples. + +## Common Queries + +### 1. Get Feed (Following) +\`\`\`${language === 'rust' ? 'rust' : 'javascript'} +${language === 'rust' ? ` +let url = format!("{}/v0/stream/following?viewer_id={}&skip=0&limit=20", NEXUS_URL, viewer_id); +let response = client.get(&url).send().await?; +let posts: Vec = response.json().await?; +` : ` +const response = await fetch( + \`\${NEXUS_URL}/v0/stream/following?viewer_id=\${viewerId}&skip=0&limit=20\` +); +const posts = await response.json(); +`} +\`\`\` + +### 2. Search Users +\`\`\`${language === 'rust' ? 'rust' : 'javascript'} +${language === 'rust' ? ` +let url = format!("{}/v0/search/users?query={}&skip=0&limit=10", NEXUS_URL, search_term); +let response = client.get(&url).send().await?; +let users: Vec = response.json().await?; +` : ` +const response = await fetch( + \`\${NEXUS_URL}/v0/search/users?query=\${searchTerm}&skip=0&limit=10\` +); +const users = await response.json(); +`} +\`\`\` + +### 3. Get Post Details +\`\`\`${language === 'rust' ? 'rust' : 'javascript'} +${language === 'rust' ? ` +let url = format!("{}/v0/post/{}/{}", NEXUS_URL, author_id, post_id); +let response = client.get(&url).send().await?; +let post: PostView = response.json().await?; +` : ` +const response = await fetch(\`\${NEXUS_URL}/v0/post/\${authorId}/\${postId}\`); +const post = await response.json(); +`} +\`\`\` + +### 4. Get User Profile +\`\`\`${language === 'rust' ? 'rust' : 'javascript'} +${language === 'rust' ? ` +let url = format!("{}/v0/user/{}", NEXUS_URL, user_id); +let response = client.get(&url).send().await?; +let user: UserView = response.json().await?; +` : ` +const response = await fetch(\`\${NEXUS_URL}/v0/user/\${userId}\`); +const user = await response.json(); +`} +\`\`\` + +## Important Notes + +βœ… **Read from NEXUS** - For social features +βœ… **No authentication needed** - Public data +βœ… **Fast queries** - Pre-indexed data +βœ… **Pagination support** - Use skip/limit +❌ **Don't write to Nexus** - Write to homeserver instead + +## Nexus URL + +- **Production**: \`https://nexus.pubky.app\` +- **Local testnet**: Check with \`get_testnet_info\` tool + +## Generate Full Client + +Use \`generate_nexus_client\` tool to generate a complete API client: +- language: ${language} +- endpoints: [leave empty for common ones] + +## Next Steps + +1. Use \`query_nexus_api\` to explore available endpoints +2. Use \`generate_nexus_client\` to generate a full client +3. Use \`build-social-feed\` for a complete feed implementation + +Want me to show you a complete example with pagination and error handling?`; + + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `Show me how to query ${queryType} data from Nexus in ${language}`, + }, + }, + { + role: 'assistant', + content: { + type: 'text', + text: prompt, + }, + }, + ], + }; + } + + private async createPostUiPrompt( + args: Record + ): Promise<{ messages: Array<{ role: string; content: { type: string; text: string } }> }> { + const language = args.language || 'javascript'; + const postType = args.post_type || 'short'; + + const prompt = `I'll help you create a post UI with validation in ${language}. + +## Understanding Post Models + +Pubky app uses structured post models with validation: + +- **short**: Up to 2,000 characters +- **long**: Up to 50,000 characters +- **image**: Post with image attachments +- **video**: Post with video attachments +- **link**: Post with embedded link +- **file**: Post with file attachments + +## Implementation Steps + +### 1. Install Dependencies + +\`\`\`bash +npm install pubky-app-specs +\`\`\` + +### 2. Create Post with Validation + +\`\`\`${language} +import { PubkySpecsBuilder, PubkyAppPostKind } from 'pubky-app-specs'; + +const userId = 'your-pubky-id'; +const specsBuilder = new PubkySpecsBuilder(userId); + +// Create a ${postType} post +function createPost(content${postType === 'image' || postType === 'video' || postType === 'file' ? ', attachments = []' : ''}) { + // Validate content length + const maxLength = ${postType === 'long' ? '50000' : '2000'}; + if (content.length > maxLength) { + throw new Error(\`Content exceeds maximum length of \${maxLength} characters\`); + } + + // Create post with validation + const { post, meta } = specsBuilder.createPost( + content, + PubkyAppPostKind.${postType.charAt(0).toUpperCase() + postType.slice(1)}, + null, // parent (for replies) + null, // embed (for reposts) + ${postType === 'image' || postType === 'video' || postType === 'file' ? 'attachments' : 'null'} + ); + + return { post, meta }; +} +\`\`\` + +### 3. Build the UI Form + +${ + language === 'typescript' + ? `\`\`\`tsx +import React, { useState } from 'react'; +import { PubkySpecsBuilder, PubkyAppPostKind } from 'pubky-app-specs'; + +export function CreatePostForm({ userId, onPostCreated }) { + const [content, setContent] = useState(''); + const [error, setError] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + + const maxLength = ${postType === 'long' ? 50000 : 2000}; + const remaining = maxLength - content.length; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + + if (content.trim().length === 0) { + setError('Content cannot be empty'); + return; + } + + if (content.length > maxLength) { + setError(\`Content exceeds maximum length of \${maxLength} characters\`); + return; + } + + setIsSubmitting(true); + + try { + const specsBuilder = new PubkySpecsBuilder(userId); + const { post, meta } = specsBuilder.createPost( + content, + PubkyAppPostKind.${postType.charAt(0).toUpperCase() + postType.slice(1)}, + null, + null, + null + ); + + // Store to Pubky (using your storage session) + await session.storage().put(meta.url, post.toJson()); + + setContent(''); + onPostCreated({ post, meta }); + } catch (err) { + setError(err.message); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+ +
+ 0 / ${postType === 'long' ? '50000' : '2000'} + +
+
+\`; + +const textarea = form.querySelector('#postContent'); +const charCount = form.querySelector('#charCount'); +const errorDiv = form.querySelector('#error'); + +// Update character count +textarea.addEventListener('input', () => { + charCount.textContent = \`\${textarea.value.length} / ${postType === 'long' ? '50000' : '2000'}\`; +}); + +// Handle submit +form.addEventListener('submit', async (e) => { + e.preventDefault(); + errorDiv.textContent = ''; + + const content = textarea.value.trim(); + if (!content) { + errorDiv.textContent = 'Content cannot be empty'; + return; + } + + try { + const specsBuilder = new PubkySpecsBuilder(userId); + const { post, meta } = specsBuilder.createPost( + content, + PubkyAppPostKind.${postType.charAt(0).toUpperCase() + postType.slice(1)}, + null, + null, + null + ); + + // Store to Pubky + await session.storage().put(meta.url, JSON.stringify(post.toJson())); + + textarea.value = ''; + alert('Post created!'); + } catch (err) { + errorDiv.textContent = err.message; + } +}); +\`\`\` +` +} + +${ + postType === 'image' || postType === 'video' || postType === 'file' + ? ` +### 4. Handle File Uploads + +\`\`\`${language} +${ + language === 'typescript' + ? `async function uploadFile(file: File, userId: string, session: PubkySession) { + // Create blob + const arrayBuffer = await file.arrayBuffer(); + const bytes = new Uint8Array(arrayBuffer); + + const specsBuilder = new PubkySpecsBuilder(userId); + const { blob, meta: blobMeta } = specsBuilder.createBlob(Array.from(bytes)); + + // Upload blob + await session.storage().put(blobMeta.url, arrayBuffer); + + // Create file metadata + const { file: fileMetadata, meta: fileMeta } = specsBuilder.createFile( + file.name, + blobMeta.url, + file.type, + file.size + ); + + // Store file metadata + await session.storage().put(fileMeta.url, fileMetadata.toJson()); + + return fileMeta.url; // Return for use in post attachments +}` + : `async function uploadFile(file, userId, session) { + const arrayBuffer = await file.arrayBuffer(); + const bytes = new Uint8Array(arrayBuffer); + + const specsBuilder = new PubkySpecsBuilder(userId); + const { blob, meta: blobMeta } = specsBuilder.createBlob(Array.from(bytes)); + + await session.storage().put(blobMeta.url, arrayBuffer); + + const { file: fileMetadata, meta: fileMeta } = specsBuilder.createFile( + file.name, + blobMeta.url, + file.type, + file.size + ); + + await session.storage().put(fileMeta.url, JSON.stringify(fileMetadata.toJson())); + + return fileMeta.url; +}` +} +\`\`\` +` + : '' +} + +## Validation Rules + +The specs automatically validate: +- βœ… Content length (${postType === 'long' ? '50,000' : '2,000'} chars max) +- βœ… Reserved keyword "[DELETED]" not allowed +- βœ… Valid URI format for parent/embed +- βœ… Post kind enum validation + +## Best Practices + +1. **Real-time Validation**: Show character count and errors as user types +2. **Sanitization**: Content is automatically trimmed and sanitized +3. **Error Handling**: Display validation errors clearly to users +4. **Loading States**: Disable submit button while posting +5. **Success Feedback**: Show confirmation after successful post + +## Next Steps + +- Use \`explain_model\` tool for detailed post model docs +- Use \`create_model_example\` for more examples +- Query created posts with \`query_nexus_api\` + +Need help with replies or reposts? Just ask!`; + + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `How do I create a ${postType} post UI with validation?`, + }, + }, + { + role: 'assistant', + content: { + type: 'text', + text: prompt, + }, + }, + ], + }; + } + + private async implementUserProfilePrompt( + args: Record + ): Promise<{ messages: Array<{ role: string; content: { type: string; text: string } }> }> { + const language = args.language || 'javascript'; + + const prompt = `I'll help you implement user profile management in ${language}. + +## User Profile Structure + +Pubky user profiles include: +- **name**: 3-50 characters (required) +- **bio**: Up to 160 characters +- **image**: Profile picture URL +- **links**: Up to 5 social links +- **status**: Current status (50 chars max) + +## Implementation Steps + +### 1. Create/Update Profile + +\`\`\`${language === 'rust' ? 'rust' : 'javascript'} +${ + language === 'rust' + ? `use pubky_app_specs::{PubkyAppUser, PubkyAppUserLink, Validatable}; + +async fn create_profile( + session: &PubkySession, + name: String, + bio: Option, + image: Option, +) -> Result<(), Box> { + let user = PubkyAppUser::new( + name, + bio, + image, + Some(vec![ + PubkyAppUserLink::new( + "GitHub".to_string(), + "https://github.com/username".to_string() + ) + ]), + Some("Building on Pubky".to_string()) + ); + + // Validate + user.validate(None)?; + + // Save to storage + let path = PubkyAppUser::create_path(); + let json = serde_json::to_string(&user)?; + session.storage().put(&path, json.as_bytes()).await?; + + Ok(()) +}` + : `import { PubkySpecsBuilder } from 'pubky-app-specs'; + +async function createProfile(session, userId, profileData) { + const specsBuilder = new PubkySpecsBuilder(userId); + + const { user, meta } = specsBuilder.createUser( + profileData.name, + profileData.bio || null, + profileData.image || null, + profileData.links || [], + profileData.status || null + ); + + // Validate (automatic in createUser) + // Save to storage + await session.storage().put(meta.url, user.toJson()); + + return { user, meta }; +}` +} +\`\`\` + +### 2. Fetch User Profile + +\`\`\`${language === 'rust' ? 'rust' : 'javascript'} +${ + language === 'rust' + ? `// Fetch via Nexus API (includes counts, tags, etc.) +async fn fetch_user_profile(user_id: &str, viewer_id: Option<&str>) -> Result { + let mut url = format!("https://nexus.example.com/v0/user/{}", user_id); + if let Some(viewer) = viewer_id { + url.push_str(&format!("?viewer_id={}", viewer)); + } + + reqwest::get(&url).await?.json().await +} + +// Or fetch directly from homeserver +async fn fetch_profile_raw(pubky: &Pubky, user_id: &str) -> Result> { + let public_storage = pubky.public_storage(); + let uri = format!("pubky://{}/pub/pubky.app/profile.json", user_id); + let response = public_storage.get(&uri).await?; + let json = response.text().await?; + let user: PubkyAppUser = serde_json::from_str(&json)?; + Ok(user) +}` + : `// Fetch via Nexus API (includes counts, tags, relationship) +async function fetchUserProfile(userId, viewerId = null) { + const params = new URLSearchParams(); + if (viewerId) params.append('viewer_id', viewerId); + + const url = \`https://nexus.example.com/v0/user/\${userId}?\${params}\`; + const response = await fetch(url); + return await response.json(); // Returns UserView with full social data +} + +// Or fetch directly from homeserver +async function fetchProfileRaw(pubky, userId) { + const publicStorage = pubky.publicStorage(); + const uri = \`pubky://\${userId}/pub/pubky.app/profile.json\`; + const response = await publicStorage.get(uri); + return JSON.parse(await response.text()); +}` +} +\`\`\` + +### 3. Build Profile UI + +${ + language === 'typescript' + ? `\`\`\`tsx +import React, { useState, useEffect } from 'react'; + +export function UserProfile({ userId, viewerId, canEdit }) { + const [profile, setProfile] = useState(null); + const [isEditing, setIsEditing] = useState(false); + + useEffect(() => { + fetchUserProfile(userId, viewerId).then(setProfile); + }, [userId, viewerId]); + + if (!profile) return
Loading...
; + + const { details, counts, relationship, tags } = profile; + + return ( +
+ {details.image && {details.name}} + +

{details.name}

+ {details.status &&

{details.status}

} + {details.bio &&

{details.bio}

} + +
+ {counts.posts} posts + {counts.followers} followers + {counts.following} following +
+ + {details.links && ( +
+ {details.links.map((link, i) => ( + + {link.title} + + ))} +
+ )} + + {relationship && ( +
+ {relationship.following && βœ“ Following} + {relationship.followed_by && Follows you} +
+ )} + + {tags.length > 0 && ( +
+ {tags.map(tag => ( + + {tag.label} ({tag.taggers_count}) + + ))} +
+ )} + + {canEdit && ( + + )} +
+ ); +} +\`\`\` +` + : `\`\`\`javascript +async function renderProfile(userId, containerId) { + const profile = await fetchUserProfile(userId); + const { details, counts, tags } = profile; + + const html = \` + + \`; + + document.getElementById(containerId).innerHTML = html; +} +\`\`\` +` +} + +### 4. Profile Edit Form + +Follow the same pattern as the post creation form: +- Use \`PubkySpecsBuilder.createUser()\` for validation +- Show character counts for bio/status +- Validate URLs for links +- Handle image uploads + +## Validation Rules + +Automatic validation includes: +- βœ… Name: 3-50 characters, not "[DELETED]" +- βœ… Bio: Max 160 characters +- βœ… Image: Valid URL, max 300 characters +- βœ… Links: Max 5, each with valid URL +- βœ… Status: Max 50 characters + +## Advanced Features + +### Follow/Unfollow + +\`\`\`${language === 'rust' ? 'rust' : 'javascript'} +${ + language === 'rust' + ? `async fn follow_user(session: &PubkySession, user_id: &str) -> Result<(), Box> { + let follow = PubkyAppFollow::new(); + let path = PubkyAppFollow::create_path(user_id); + let json = serde_json::to_string(&follow)?; + session.storage().put(&path, json.as_bytes()).await?; + Ok(()) +}` + : `async function followUser(session, userId, targetUserId) { + const specsBuilder = new PubkySpecsBuilder(userId); + const { follow, meta } = specsBuilder.createFollow(targetUserId); + await session.storage().put(meta.url, follow.toJson()); +}` +} +\`\`\` + +### Tag Users + +\`\`\`${language === 'rust' ? 'rust' : 'javascript'} +${ + language === 'rust' + ? `async fn tag_user(session: &PubkySession, target_uri: &str, label: &str) -> Result<(), Box> { + let tag = PubkyAppTag::new(target_uri.to_string(), label.to_string()); + let tag_id = tag.create_id(); + let path = PubkyAppTag::create_path(&tag_id); + let json = serde_json::to_string(&tag)?; + session.storage().put(&path, json.as_bytes()).await?; + Ok(()) +}` + : `async function tagUser(session, userId, targetUri, label) { + const specsBuilder = new PubkySpecsBuilder(userId); + const { tag, meta } = specsBuilder.createTag(targetUri, label); + await session.storage().put(meta.url, tag.toJson()); +}` +} +\`\`\` + +## Next Steps + +- Use \`query_nexus_api\` for user search endpoints +- Use \`explain_model\` to understand UserView schema +- Build a user directory with \`/v0/stream/users\` + +Need help with specific features? Just ask!`; + + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `How do I implement user profile management in ${language}?`, + }, + }, + { + role: 'assistant', + content: { + type: 'text', + text: prompt, + }, + }, + ], + }; + } + + private async querySocialDataPrompt( + args: Record + ): Promise<{ messages: Array<{ role: string; content: { type: string; text: string } }> }> { + const dataType = args.data_type || 'posts'; + const language = args.language || 'javascript'; + + const endpointMap: Record = { + posts: { + endpoint: '/v0/stream/posts', + description: 'Stream posts with filtering and pagination', + }, + users: { + endpoint: '/v0/stream/users', + description: 'Stream users with various sources', + }, + tags: { + endpoint: '/v0/tags/hot', + description: 'Get trending tags', + }, + streams: { + endpoint: '/v0/stream/posts', + description: 'Various post streams', + }, + }; + + const info = endpointMap[dataType.toLowerCase()] || endpointMap.posts; + + const prompt = `I'll show you how to query ${dataType} from the Nexus API in ${language}. + +## Endpoint: ${info.endpoint} + +${info.description} + +## Basic Query + +\`\`\`${language === 'rust' ? 'rust' : 'javascript'} +${ + language === 'rust' + ? `use reqwest; + +async fn query_${dataType}() -> Result, reqwest::Error> { + let url = "https://nexus.example.com${info.endpoint}"; + let response = reqwest::get(url).await?; + response.json().await +}` + : `async function query${dataType.charAt(0).toUpperCase() + dataType.slice(1)}() { + const response = await fetch('https://nexus.example.com${info.endpoint}'); + return await response.json(); +}` +} +\`\`\` + +## Advanced Queries + +### With Pagination + +\`\`\`${language === 'rust' ? 'rust' : 'javascript'} +${ + language === 'rust' + ? `async fn query_with_pagination(skip: u32, limit: u32) -> Result, reqwest::Error> { + let url = format!("https://nexus.example.com${info.endpoint}?skip={}&limit={}", skip, limit); + reqwest::get(&url).await?.json().await +}` + : `async function queryWithPagination(skip = 0, limit = 20) { + const params = new URLSearchParams({ skip, limit }); + const response = await fetch(\`https://nexus.example.com${info.endpoint}?\${params}\`); + return await response.json(); +}` +} +\`\`\` + +### With Filtering + +\`\`\`${language === 'rust' ? 'rust' : 'javascript'} +${ + language === 'rust' + ? `async fn query_with_filters( + source: &str, + observer_id: &str, + tags: Vec, +) -> Result, reqwest::Error> { + let tags_str = tags.join(","); + let url = format!( + "https://nexus.example.com${info.endpoint}?source={}&observer_id={}&tags={}", + source, observer_id, tags_str + ); + reqwest::get(&url).await?.json().await +}` + : `async function queryWithFilters(source, observerId, tags = []) { + const params = new URLSearchParams({ + source, + observer_id: observerId, + tags: tags.join(',') + }); + const response = await fetch(\`https://nexus.example.com${info.endpoint}?\${params}\`); + return await response.json(); +}` +} +\`\`\` + +## Complete Example + +\`\`\`${language === 'rust' ? 'rust' : 'javascript'} +${ + language === 'rust' + ? `use reqwest; +use serde::Deserialize; + +#[derive(Deserialize)] +struct QueryResult { + // Define based on endpoint response +} + +async fn advanced_query() -> Result<(), Box> { + let client = reqwest::Client::new(); + + let response = client + .get("https://nexus.example.com${info.endpoint}") + .query(&[ + ("skip", "0"), + ("limit", "20"), + ("sorting", "timeline"), + ]) + .send() + .await?; + + let data: Vec = response.json().await?; + + for item in data { + // Process each item + } + + Ok(()) +}` + : `class NexusClient { + constructor(baseUrl = 'https://nexus.example.com') { + this.baseUrl = baseUrl; + } + + async query${dataType.charAt(0).toUpperCase() + dataType.slice(1)}(options = {}) { + const params = new URLSearchParams({ + skip: options.skip || 0, + limit: options.limit || 20, + ...options.filters + }); + + const response = await fetch(\`\${this.baseUrl}${info.endpoint}?\${params}\`); + + if (!response.ok) { + throw new Error(\`HTTP error! status: \${response.status}\`); + } + + return await response.json(); + } +} + +// Usage +const client = new NexusClient(); +const results = await client.query${dataType.charAt(0).toUpperCase() + dataType.slice(1)}({ + skip: 0, + limit: 20, + filters: { + sorting: 'timeline', + source: 'all' + } +}); + +console.log(\`Found \${results.length} results\`); +results.forEach(item => console.log(item));` +} +\`\`\` + +## Available Query Parameters + +Use \`explain_nexus_endpoint\` tool to see all available parameters for specific endpoints. + +Common parameters: +- **skip**: Number of items to skip (pagination) +- **limit**: Maximum number of items to return +- **sorting**: Sort method (timeline, total_engagement) +- **source**: Data source (all, following, friends, etc.) +- **viewer_id**: Personalize results for a specific user +- **tags**: Filter by comma-separated tags + +## Error Handling + +\`\`\`${language === 'rust' ? 'rust' : 'javascript'} +${ + language === 'rust' + ? `match query_${dataType}().await { + Ok(results) => println!("Got {} results", results.len()), + Err(e) => eprintln!("Query failed: {}", e), +}` + : `try { + const results = await query${dataType.charAt(0).toUpperCase() + dataType.slice(1)}(); + console.log(\`Got \${results.length} results\`); +} catch (error) { + if (error.response?.status === 404) { + console.log('No results found'); + } else if (error.response?.status === 500) { + console.error('Server error'); + } else { + console.error('Query failed:', error); + } +}` +} +\`\`\` + +## Next Steps + +- Use \`query_nexus_api\` to explore more endpoints +- Use \`generate_nexus_client\` to create a full client +- Check \`pubky://api/nexus/schemas\` for response structures + +Need help with a specific query? Just ask!`; + + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `How do I query ${dataType} from Nexus API?`, + }, + }, + { + role: 'assistant', + content: { + type: 'text', + text: prompt, + }, + }, + ], + }; + } + + private async validateAppDataPrompt( + args: Record + ): Promise<{ messages: Array<{ role: string; content: { type: string; text: string } }> }> { + const model = args.model || 'user'; + + const prompt = `I'll show you how to validate ${model} data using Pubky app specs. + +## Understanding Validation + +Pubky app specs provide automatic validation for all data models: +- **Type checking**: Ensures correct data types +- **Length limits**: Enforces character/size limits +- **Format validation**: URLs, URIs, enums +- **Sanitization**: Automatic trimming and normalization + +## Using the Validation System + +### JavaScript/TypeScript + +\`\`\`javascript +import { PubkySpecsBuilder } from 'pubky-app-specs'; + +const userId = 'your-pubky-id'; +const specsBuilder = new PubkySpecsBuilder(userId); + +// Method 1: Use builder (automatic validation) +try { + const { ${model}, meta } = specsBuilder.create${model.charAt(0).toUpperCase() + model.slice(1)}( + /* your data */ + ); + console.log('βœ“ Data is valid!'); +} catch (error) { + console.error('Validation failed:', error.message); +} + +// Method 2: Manual validation (advanced) +import { PubkyApp${model.charAt(0).toUpperCase() + model.slice(1)} } from 'pubky-app-specs'; + +const data = PubkyApp${model.charAt(0).toUpperCase() + model.slice(1)}.fromJson({ + /* your JSON data */ +}); + +// Data is automatically sanitized and validated +\`\`\` + +### Rust + +\`\`\`rust +use pubky_app_specs::{PubkyApp${model.charAt(0).toUpperCase() + model.slice(1)}, Validatable}; + +// Create with automatic sanitization +let ${model} = PubkyApp${model.charAt(0).toUpperCase() + model.slice(1)}::new(/* parameters */); + +// Validate +match ${model}.validate(None) { + Ok(_) => println!("βœ“ Data is valid!"), + Err(e) => eprintln!("Validation failed: {}", e), +} + +// Or use try_from for JSON validation +let json = r#"{ /* your JSON */ }"#; +match PubkyApp${model.charAt(0).toUpperCase() + model.slice(1)}::try_from(json.as_bytes(), "") { + Ok(${model}) => println!("βœ“ Valid and deserialized!"), + Err(e) => eprintln!("Invalid: {}", e), +} +\`\`\` + +## Validation Rules for ${model} + +Use the \`explain_model\` tool to see complete validation rules: + +\`\`\`javascript +// Check validation rules +const rules = await explainModel('${model}'); +console.log(rules); +\`\`\` + +## Common Validation Errors + +### 1. Length Violations + +\`\`\`javascript +// ❌ Too short/long +const { user } = specsBuilder.createUser( + 'AB', // Too short (min 3 chars) + null, null, null, null +); +// Error: "Validation Error: Invalid name length" +\`\`\` + +### 2. Invalid URLs + +\`\`\`javascript +// ❌ Malformed URL +const { user } = specsBuilder.createUser( + 'Alice', + null, + 'not-a-url', // Invalid + null, null +); +// Error: Invalid URL or automatically sanitized to null +\`\`\` + +### 3. Reserved Keywords + +\`\`\`javascript +// ❌ Reserved keyword +const { post } = specsBuilder.createPost( + '[DELETED]', // Reserved + PubkyAppPostKind.Short, + null, null, null +); +// Automatically sanitized to "empty" +\`\`\` + +## Pre-flight Validation + +Validate before submission: + +\`\`\`javascript +function validateBeforeSubmit(formData) { + const errors = {}; + + // Check name length + if (formData.name.length < 3 || formData.name.length > 50) { + errors.name = 'Name must be 3-50 characters'; + } + + // Check bio length + if (formData.bio && formData.bio.length > 160) { + errors.bio = 'Bio must be 160 characters or less'; + } + + // Check URL format + if (formData.image) { + try { + new URL(formData.image); + } catch { + errors.image = 'Invalid URL format'; + } + } + + // Check links + if (formData.links && formData.links.length > 5) { + errors.links = 'Maximum 5 links allowed'; + } + + return { + isValid: Object.keys(errors).length === 0, + errors + }; +} + +// Use in form +const validation = validateBeforeSubmit(formData); +if (!validation.isValid) { + displayErrors(validation.errors); + return; +} + +// Proceed with creation +const { ${model} } = specsBuilder.create${model.charAt(0).toUpperCase() + model.slice(1)}(/* ... */); +\`\`\` + +## Sanitization Features + +The specs automatically sanitize data: + +1. **Trimming**: Whitespace removed +2. **Length enforcement**: Truncates to max length +3. **URL normalization**: Converts to valid URLs +4. **Lowercasing**: For tags and labels +5. **Reserved word filtering**: Replaces forbidden keywords + +\`\`\`javascript +// Input +const { user } = specsBuilder.createUser( + ' Alice ', // Extra whitespace + ' Developer and designer ', // Whitespace in bio + null, null, null +); + +// Output (automatically sanitized) +console.log(user.name); // 'Alice' (trimmed) +console.log(user.bio); // 'Developer and designer' (trimmed) +\`\`\` + +## Testing Validation + +\`\`\`javascript +// Unit test example +describe('${model} validation', () => { + it('should accept valid data', () => { + const { ${model} } = specsBuilder.create${model.charAt(0).toUpperCase() + model.slice(1)}(/* valid data */); + expect(${model}).toBeDefined(); + }); + + it('should reject invalid data', () => { + expect(() => { + specsBuilder.create${model.charAt(0).toUpperCase() + model.slice(1)}(/* invalid data */); + }).toThrow(); + }); +}); +\`\`\` + +## Best Practices + +1. βœ… Validate early - check data before API calls +2. βœ… Display validation errors clearly to users +3. βœ… Use real-time validation in forms +4. βœ… Trust the automatic sanitization +5. βœ… Test edge cases (max length, special chars, etc.) + +## Next Steps + +- Use \`explain_model\` for complete ${model} validation rules +- Use \`create_model_example\` for working examples +- Use \`validate_model_data\` tool to test JSON data + +Need help with specific validation? Just ask!`; + + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `How do I validate ${model} data?`, + }, + }, + { + role: 'assistant', + content: { + type: 'text', + text: prompt, + }, + }, + ], + }; + } +} diff --git a/pubky-mcp-server/src/resources.ts b/pubky-mcp-server/src/resources.ts new file mode 100644 index 0000000..8f5441f --- /dev/null +++ b/pubky-mcp-server/src/resources.ts @@ -0,0 +1,930 @@ +/** + * MCP Resources for Pubky documentation and examples + */ + +import { Resource } from '@modelcontextprotocol/sdk/types.js'; +import { FileReader } from './utils/file-reader.js'; +import { RESOURCE_TYPES, DOC_SECTIONS, API_SECTIONS } from './constants.js'; +import { NexusApiParser } from './utils/nexus-api.js'; +import { AppSpecsParser } from './utils/app-specs.js'; +import * as path from 'path'; + +export class ResourceHandler { + private nexusParser: NexusApiParser; + private specsParser: AppSpecsParser; + + constructor( + private fileReader: FileReader, + workspaceRoot: string + ) { + this.nexusParser = new NexusApiParser(workspaceRoot); + this.specsParser = new AppSpecsParser(workspaceRoot); + } + + async listResources(): Promise { + return [ + // Documentation resources + { + uri: 'pubky://docs/overview', + name: 'Pubky Protocol Overview', + mimeType: 'text/markdown', + description: 'Overview of the Pubky protocol and its features', + }, + { + uri: 'pubky://docs/concepts/rootkey', + name: 'Root Key Concept', + mimeType: 'text/markdown', + description: 'Understanding Pubky root keys and identity', + }, + { + uri: 'pubky://docs/concepts/homeserver', + name: 'Homeserver Concept', + mimeType: 'text/markdown', + description: 'Understanding Pubky homeservers', + }, + { + uri: 'pubky://docs/auth', + name: 'Authentication Flow', + mimeType: 'text/markdown', + description: 'Pubky Auth protocol and 3rd party authorization', + }, + + // Example resources + { + uri: 'pubky://examples/rust/all', + name: 'All Rust Examples', + mimeType: 'text/markdown', + description: 'Complete list of Rust examples', + }, + { + uri: 'pubky://examples/javascript/all', + name: 'All JavaScript Examples', + mimeType: 'text/markdown', + description: 'Complete list of JavaScript examples', + }, + + // API references + { + uri: 'pubky://api/homeserver', + name: 'Homeserver API Reference', + mimeType: 'text/markdown', + description: 'HTTP API endpoints provided by Pubky homeserver', + }, + { + uri: 'pubky://api/sdk', + name: 'SDK API Reference', + mimeType: 'text/markdown', + description: 'Pubky SDK API documentation', + }, + { + uri: 'pubky://api/capabilities', + name: 'Capabilities Reference', + mimeType: 'text/markdown', + description: 'Complete guide to Pubky capabilities and permissions', + }, + + // Nexus API resources + { + uri: 'pubky://api/nexus/overview', + name: 'Nexus API Overview', + mimeType: 'text/markdown', + description: 'Overview of the Nexus social API', + }, + { + uri: 'pubky://api/nexus/endpoints/posts', + name: 'Nexus Post Endpoints', + mimeType: 'text/markdown', + description: 'Post-related API endpoints', + }, + { + uri: 'pubky://api/nexus/endpoints/users', + name: 'Nexus User Endpoints', + mimeType: 'text/markdown', + description: 'User-related API endpoints', + }, + { + uri: 'pubky://api/nexus/endpoints/streams', + name: 'Nexus Stream Endpoints', + mimeType: 'text/markdown', + description: 'Stream and feed API endpoints', + }, + { + uri: 'pubky://api/nexus/endpoints/search', + name: 'Nexus Search Endpoints', + mimeType: 'text/markdown', + description: 'Search API endpoints', + }, + { + uri: 'pubky://api/nexus/endpoints/tags', + name: 'Nexus Tag Endpoints', + mimeType: 'text/markdown', + description: 'Tag-related API endpoints', + }, + { + uri: 'pubky://api/nexus/schemas', + name: 'Nexus API Schemas', + mimeType: 'text/markdown', + description: 'All data schemas from Nexus API', + }, + + // Pubky App Specs resources + { + uri: 'pubky://specs/overview', + name: 'Pubky App Specs Overview', + mimeType: 'text/markdown', + description: 'Overview of Pubky app data model specifications', + }, + { + uri: 'pubky://specs/models/user', + name: 'PubkyAppUser Model', + mimeType: 'text/markdown', + description: 'User profile data model specification', + }, + { + uri: 'pubky://specs/models/post', + name: 'PubkyAppPost Model', + mimeType: 'text/markdown', + description: 'Post data model specification', + }, + { + uri: 'pubky://specs/models/tag', + name: 'PubkyAppTag Model', + mimeType: 'text/markdown', + description: 'Tag data model specification', + }, + { + uri: 'pubky://specs/models/bookmark', + name: 'PubkyAppBookmark Model', + mimeType: 'text/markdown', + description: 'Bookmark data model specification', + }, + { + uri: 'pubky://specs/models/follow', + name: 'PubkyAppFollow Model', + mimeType: 'text/markdown', + description: 'Follow relationship data model specification', + }, + { + uri: 'pubky://specs/models/file', + name: 'PubkyAppFile Model', + mimeType: 'text/markdown', + description: 'File metadata data model specification', + }, + { + uri: 'pubky://specs/models/feed', + name: 'PubkyAppFeed Model', + mimeType: 'text/markdown', + description: 'Feed configuration data model specification', + }, + { + uri: 'pubky://specs/examples', + name: 'Pubky App Specs Examples', + mimeType: 'text/markdown', + description: 'JavaScript usage examples for all data models', + }, + + // Pkarr resources + { + uri: 'pkarr://overview', + name: 'Pkarr Overview', + mimeType: 'text/markdown', + description: 'Overview of Pkarr - Public-Key Addressable Resource Records', + }, + { + uri: 'pkarr://design/base', + name: 'Pkarr Base Specification', + mimeType: 'text/markdown', + description: 'Core Pkarr protocol specification', + }, + { + uri: 'pkarr://design/relays', + name: 'Pkarr Relay Specification', + mimeType: 'text/markdown', + description: 'HTTP relay servers for Pkarr', + }, + { + uri: 'pkarr://design/endpoints', + name: 'Pkarr Endpoints Specification', + mimeType: 'text/markdown', + description: 'Resolving query names to endpoints using SVCB records', + }, + { + uri: 'pkarr://design/tls', + name: 'Pkarr TLS Specification', + mimeType: 'text/markdown', + description: 'End-to-end encryption for Pkarr domains', + }, + { + uri: 'pkarr://design/resolvers', + name: 'Pkarr Resolvers Specification', + mimeType: 'text/markdown', + description: 'DNS resolver specification for Pkarr', + }, + { + uri: 'pkarr://examples/rust/publish', + name: 'Pkarr Publish Example (Rust)', + mimeType: 'text/rust', + description: 'Example of publishing signed packets to DHT', + }, + { + uri: 'pkarr://examples/rust/resolve', + name: 'Pkarr Resolve Example (Rust)', + mimeType: 'text/rust', + description: 'Example of resolving public keys from DHT', + }, + { + uri: 'pkarr://examples/rust/http-serve', + name: 'Pkarr HTTP Serve Example (Rust)', + mimeType: 'text/rust', + description: 'Example HTTP server listening on a Pkarr key', + }, + { + uri: 'pkarr://examples/rust/http-get', + name: 'Pkarr HTTP Get Example (Rust)', + mimeType: 'text/rust', + description: 'Example HTTP client resolving Pkarr domains', + }, + { + uri: 'pkarr://examples/javascript', + name: 'Pkarr JavaScript Examples', + mimeType: 'text/markdown', + description: 'JavaScript bindings and usage examples', + }, + { + uri: 'pkarr://relay/config', + name: 'Pkarr Relay Configuration', + mimeType: 'text/toml', + description: 'Example configuration file for Pkarr relay', + }, + + // Pkdns resources (DNS resolver for Pkarr domains) + { + uri: 'pkdns://overview', + name: 'Pkdns Overview', + mimeType: 'text/markdown', + description: 'DNS server for Pkarr domains - makes public keys work as TLDs', + }, + { + uri: 'pkdns://docs/dns-over-https', + name: 'DNS-over-HTTPS Setup', + mimeType: 'text/markdown', + description: 'Setting up DNS-over-HTTPS with pkdns', + }, + { + uri: 'pkdns://docs/dyn-dns', + name: 'DynDNS Configuration', + mimeType: 'text/markdown', + description: 'Dynamic DNS setup with pkdns', + }, + { + uri: 'pkdns://config', + name: 'Pkdns Server Configuration', + mimeType: 'text/toml', + description: 'Example server configuration for pkdns', + }, + { + uri: 'pkdns://servers', + name: 'Public Pkdns Servers', + mimeType: 'text/plain', + description: 'List of public pkdns DNS servers', + }, + + // Pubky Nexus resources (social indexer implementation) + { + uri: 'nexus://overview', + name: 'Pubky Nexus Overview', + mimeType: 'text/markdown', + description: 'Social graph indexer - architecture and implementation', + }, + { + uri: 'nexus://architecture', + name: 'Nexus Architecture', + mimeType: 'text/markdown', + description: 'Watcher, service, and database architecture', + }, + { + uri: 'nexus://component/watcher', + name: 'Nexus Watcher', + mimeType: 'text/markdown', + description: 'Event aggregator and indexing component', + }, + { + uri: 'nexus://component/service', + name: 'Nexus Service API', + mimeType: 'text/markdown', + description: 'REST API server component', + }, + { + uri: 'nexus://component/common', + name: 'Nexus Common', + mimeType: 'text/markdown', + description: 'Shared library for database and models', + }, + { + uri: 'nexus://setup/development', + name: 'Nexus Development Setup', + mimeType: 'text/markdown', + description: 'Setting up Neo4j, Redis, and Docker for development', + }, + { + uri: 'nexus://examples', + name: 'Nexus Code Examples', + mimeType: 'text/markdown', + description: 'Example code for watcher and API usage', + }, + ]; + } + + async getResource( + uri: string + ): Promise<{ contents: { uri: string; mimeType: string; text: string }[] }> { + // Handle different URI schemes + if (uri.startsWith('pkarr://')) { + return await this.getPkarrResource(uri); + } + if (uri.startsWith('pkdns://')) { + return await this.getPkdnsResource(uri); + } + if (uri.startsWith('nexus://')) { + return await this.getNexusResource(uri); + } + + // Parse the URI + const uriPath = uri.replace('pubky://', ''); + const parts = uriPath.split('/'); + + try { + switch (true) { + case parts[0] === RESOURCE_TYPES.DOCS: + return await this.getDocResource(parts.slice(1)); + case parts[0] === RESOURCE_TYPES.EXAMPLES: + return await this.getExampleResource(parts.slice(1)); + case parts[0] === RESOURCE_TYPES.API: + return await this.getApiResource(parts.slice(1)); + case parts[0] === RESOURCE_TYPES.SPECS: + return await this.getSpecsResource(parts.slice(1)); + default: + throw new Error(`Unknown resource type: ${parts[0]}`); + } + } catch (error: any) { + throw new Error(`Failed to load resource ${uri}: ${error.message}`); + } + } + + private async getDocResource( + parts: string[] + ): Promise<{ contents: { uri: string; mimeType: string; text: string }[] }> { + const paths = this.fileReader.getPaths(); + + switch (true) { + case parts[0] === DOC_SECTIONS.OVERVIEW: { + const overview = await this.fileReader.readDocFile('src/overview.md'); + const readme = await this.fileReader.readFile(path.join(paths.root, 'README.md')); + return { + contents: [ + { + uri: 'pubky://docs/overview', + mimeType: 'text/markdown', + text: `# Pubky Protocol Overview\n\n${readme}\n\n---\n\n${overview}`, + }, + ], + }; + } + case parts[0] === DOC_SECTIONS.CONCEPTS: { + const conceptFile = parts[1] === 'rootkey' ? 'rootkey.md' : 'homeserver.md'; + const content = await this.fileReader.readDocFile(`src/concepts/${conceptFile}`); + return { + contents: [ + { + uri: `pubky://docs/concepts/${parts[1]}`, + mimeType: 'text/markdown', + text: content, + }, + ], + }; + } + case parts[0] === DOC_SECTIONS.AUTH: { + const content = await this.fileReader.readDocFile('src/spec/auth.md'); + return { + contents: [ + { + uri: 'pubky://docs/auth', + mimeType: 'text/markdown', + text: content, + }, + ], + }; + } + default: + throw new Error(`Unknown doc resource: ${parts.join('/')}`); + } + } + + private async getExampleResource( + parts: string[] + ): Promise<{ contents: { uri: string; mimeType: string; text: string }[] }> { + const language = parts[0] as 'rust' | 'javascript'; + const paths = this.fileReader.getPaths(); + + if (parts[1] === 'all') { + const examplePath = language === 'rust' ? paths.examplesRust : paths.examplesJs; + const readme = await this.fileReader.readFile(path.join(examplePath, 'README.md')); + + // List all example directories + const entries = await this.fileReader.listDirectory(examplePath); + const examples = entries.filter( + e => !e.startsWith('.') && e !== 'README.md' && e !== 'Cargo.toml' && e !== 'package.json' + ); + + let content = `# ${language === 'rust' ? 'Rust' : 'JavaScript'} Examples\n\n${readme}\n\n## Available Examples\n\n`; + + for (const example of examples) { + try { + const exampleReadme = await this.fileReader.readExampleFile( + language, + `${example}/README.md` + ); + content += `### ${example}\n\n${exampleReadme}\n\n---\n\n`; + } catch { + // Skip if no README + } + } + + return { + contents: [ + { + uri: `pubky://examples/${language}/all`, + mimeType: 'text/markdown', + text: content, + }, + ], + }; + } else { + // Specific example + const exampleName = parts[1]; + const readme = await this.fileReader.readExampleFile(language, `${exampleName}/README.md`); + + // Try to get the main code file + let codeContent = ''; + try { + const codeFile = + language === 'rust' + ? await this.fileReader.readExampleFile(language, `${exampleName}/main.rs`) + : await this.fileReader.readExampleFile(language, `${exampleName}.mjs`); + codeContent = `\n\n## Code\n\n\`\`\`${language === 'rust' ? 'rust' : 'javascript'}\n${codeFile}\n\`\`\``; + } catch { + // No code file found + } + + return { + contents: [ + { + uri: `pubky://examples/${language}/${exampleName}`, + mimeType: 'text/markdown', + text: `${readme}${codeContent}`, + }, + ], + }; + } + } + + private async getApiResource( + parts: string[] + ): Promise<{ contents: { uri: string; mimeType: string; text: string }[] }> { + const paths = this.fileReader.getPaths(); + + switch (true) { + case parts[0] === API_SECTIONS.HOMESERVER: { + const readme = await this.fileReader.readFile( + path.join(paths.root, 'pubky-homeserver/README.md') + ); + const routesInfo = await this.generateHomeserverApiDocs(); + + return { + contents: [ + { + uri: 'pubky://api/homeserver', + mimeType: 'text/markdown', + text: `# Pubky Homeserver API\n\n${readme}\n\n---\n\n${routesInfo}`, + }, + ], + }; + } + case parts[0] === API_SECTIONS.SDK: { + const readme = await this.fileReader.readFile(path.join(paths.root, 'pubky-sdk/README.md')); + + return { + contents: [ + { + uri: 'pubky://api/sdk', + mimeType: 'text/markdown', + text: readme, + }, + ], + }; + } + case parts[0] === API_SECTIONS.CAPABILITIES: { + const capabilitiesCode = await this.fileReader.readFile( + path.join(paths.root, 'pubky-common/src/capabilities.rs') + ); + + return { + contents: [ + { + uri: 'pubky://api/capabilities', + mimeType: 'text/markdown', + text: `# Pubky Capabilities Reference\n\n\`\`\`rust\n${capabilitiesCode}\n\`\`\``, + }, + ], + }; + } + case parts[0] === API_SECTIONS.NEXUS: { + return await this.getNexusApiResource(parts.slice(1)); + } + default: + throw new Error(`Unknown API resource: ${parts.join('/')}`); + } + } + + private async getNexusApiResource( + parts: string[] + ): Promise<{ contents: { uri: string; mimeType: string; text: string }[] }> { + if (parts[0] === 'overview') { + const overview = await this.nexusParser.getOverview(); + return { + contents: [ + { + uri: 'pubky://api/nexus/overview', + mimeType: 'text/markdown', + text: overview, + }, + ], + }; + } else if (parts[0] === 'schemas') { + const schemas = await this.nexusParser.getSchemas(); + return { + contents: [ + { + uri: 'pubky://api/nexus/schemas', + mimeType: 'text/markdown', + text: schemas, + }, + ], + }; + } else if (parts[0] === 'endpoints' && parts[1]) { + const category = parts[1].charAt(0).toUpperCase() + parts[1].slice(1); + const endpoints = await this.nexusParser.getEndpointsByCategory(category); + return { + contents: [ + { + uri: `pubky://api/nexus/endpoints/${parts[1]}`, + mimeType: 'text/markdown', + text: endpoints, + }, + ], + }; + } else { + throw new Error(`Unknown Nexus API resource: ${parts.join('/')}`); + } + } + + private async getSpecsResource( + parts: string[] + ): Promise<{ contents: { uri: string; mimeType: string; text: string }[] }> { + if (parts[0] === 'overview') { + const overview = await this.specsParser.getOverview(); + return { + contents: [ + { + uri: 'pubky://specs/overview', + mimeType: 'text/markdown', + text: overview, + }, + ], + }; + } else if (parts[0] === 'examples') { + const examples = await this.specsParser.getJavaScriptExamples(); + return { + contents: [ + { + uri: 'pubky://specs/examples', + mimeType: 'text/markdown', + text: examples, + }, + ], + }; + } else if (parts[0] === 'models' && parts[1]) { + const modelInfo = await this.specsParser.getModelInfo(parts[1]); + return { + contents: [ + { + uri: `pubky://specs/models/${parts[1]}`, + mimeType: 'text/markdown', + text: modelInfo, + }, + ], + }; + } else { + throw new Error(`Unknown specs resource: ${parts.join('/')}`); + } + } + + private async getPkarrResource( + uri: string + ): Promise<{ contents: { uri: string; mimeType: string; text: string }[] }> { + const uriPath = uri.replace('pkarr://', ''); + const parts = uriPath.split('/'); + + try { + // Overview + if (uriPath === 'overview') { + const readme = await this.fileReader.readPkarrFile('README.md'); + return { + contents: [ + { + uri: 'pkarr://overview', + mimeType: 'text/markdown', + text: readme, + }, + ], + }; + } + + // Design documents + if (parts[0] === 'design' && parts[1]) { + const content = await this.fileReader.readPkarrDesignDoc(parts[1]); + return { + contents: [ + { + uri: `pkarr://design/${parts[1]}`, + mimeType: 'text/markdown', + text: content, + }, + ], + }; + } + + // Rust examples + if (parts[0] === 'examples' && parts[1] === 'rust' && parts[2]) { + const exampleName = parts[2]; + const content = await this.fileReader.readPkarrExample(exampleName); + const readme = await this.fileReader.readPkarrFile('pkarr/examples/README.md'); + return { + contents: [ + { + uri: `pkarr://examples/rust/${exampleName}`, + mimeType: 'text/rust', + text: `# Pkarr ${exampleName} Example\n\n${readme}\n\n---\n\n## Source Code\n\n\`\`\`rust\n${content}\n\`\`\``, + }, + ], + }; + } + + // JavaScript examples + if (parts[0] === 'examples' && parts[1] === 'javascript') { + const pkgReadme = await this.fileReader.readPkarrJsBindings('pkg/README.md'); + const exampleJs = await this.fileReader.readPkarrJsBindings('pkg/example.js'); + return { + contents: [ + { + uri: 'pkarr://examples/javascript', + mimeType: 'text/markdown', + text: `${pkgReadme}\n\n---\n\n## Example Code\n\n\`\`\`javascript\n${exampleJs}\n\`\`\``, + }, + ], + }; + } + + // Relay config + if (parts[0] === 'relay' && parts[1] === 'config') { + const config = await this.fileReader.readPkarrRelayConfig(); + return { + contents: [ + { + uri: 'pkarr://relay/config', + mimeType: 'text/toml', + text: `# Pkarr Relay Configuration Example\n\n\`\`\`toml\n${config}\n\`\`\``, + }, + ], + }; + } + + throw new Error(`Unknown Pkarr resource: ${uriPath}`); + } catch (error: any) { + throw new Error(`Failed to load Pkarr resource ${uri}: ${error.message}`); + } + } + + private async getPkdnsResource( + uri: string + ): Promise<{ contents: { uri: string; mimeType: string; text: string }[] }> { + const uriPath = uri.replace('pkdns://', ''); + const parts = uriPath.split('/'); + + try { + if (uriPath === 'overview') { + const readme = await this.fileReader.readPkdnsFile('README.md'); + return { + contents: [ + { + uri: 'pkdns://overview', + mimeType: 'text/markdown', + text: readme, + }, + ], + }; + } + + if (parts[0] === 'docs' && parts[1]) { + const content = await this.fileReader.readPkdnsDoc(parts[1]); + return { + contents: [ + { + uri: `pkdns://docs/${parts[1]}`, + mimeType: 'text/markdown', + text: content, + }, + ], + }; + } + + if (uriPath === 'config') { + const config = await this.fileReader.readPkdnsFile('server/config.sample.toml'); + return { + contents: [ + { + uri: 'pkdns://config', + mimeType: 'text/toml', + text: `# Pkdns Server Configuration\n\n\`\`\`toml\n${config}\n\`\`\``, + }, + ], + }; + } + + if (uriPath === 'servers') { + const servers = await this.fileReader.readPkdnsFile('servers.txt'); + return { + contents: [ + { + uri: 'pkdns://servers', + mimeType: 'text/plain', + text: `# Public Pkdns Servers\n\n${servers}`, + }, + ], + }; + } + + throw new Error(`Unknown Pkdns resource: ${uriPath}`); + } catch (error: any) { + throw new Error(`Failed to load Pkdns resource ${uri}: ${error.message}`); + } + } + + private async getNexusResource( + uri: string + ): Promise<{ contents: { uri: string; mimeType: string; text: string }[] }> { + const uriPath = uri.replace('nexus://', ''); + const parts = uriPath.split('/'); + + try { + if (uriPath === 'overview') { + const readme = await this.fileReader.readNexusFile('README.md'); + return { + contents: [ + { + uri: 'nexus://overview', + mimeType: 'text/markdown', + text: readme, + }, + ], + }; + } + + if (uriPath === 'architecture') { + const readme = await this.fileReader.readNexusFile('README.md'); + const docsReadme = await this.fileReader.readNexusDoc('readme.md'); + return { + contents: [ + { + uri: 'nexus://architecture', + mimeType: 'text/markdown', + text: `# Nexus Architecture\n\n${readme}\n\n---\n\n${docsReadme}`, + }, + ], + }; + } + + if (parts[0] === 'component' && parts[1]) { + const component = parts[1] as 'common' | 'watcher' | 'webapi'; + const content = await this.fileReader.readNexusComponentReadme(component); + return { + contents: [ + { + uri: `nexus://component/${component}`, + mimeType: 'text/markdown', + text: content, + }, + ], + }; + } + + if (parts[0] === 'setup' && parts[1] === 'development') { + const readme = await this.fileReader.readNexusFile('README.md'); + // Extract setup section + return { + contents: [ + { + uri: 'nexus://setup/development', + mimeType: 'text/markdown', + text: readme, + }, + ], + }; + } + + if (uriPath === 'examples') { + const examplesReadme = await this.fileReader.readNexusFile('examples/README.md'); + return { + contents: [ + { + uri: 'nexus://examples', + mimeType: 'text/markdown', + text: examplesReadme, + }, + ], + }; + } + + throw new Error(`Unknown Nexus resource: ${uriPath}`); + } catch (error: any) { + throw new Error(`Failed to load Nexus resource ${uri}: ${error.message}`); + } + } + + private async generateHomeserverApiDocs(): Promise { + return `## HTTP API Endpoints + +### Authentication + +#### POST /signup +Sign up a new user on the homeserver. +- Requires: AuthToken in request body +- Optional: Signup token (if homeserver requires it) +- Returns: Session ID cookie + +#### POST /session +Sign in an existing user. +- Requires: AuthToken in request body +- Returns: Session ID cookie + +#### DELETE /session +Sign out (delete current session). +- Requires: Valid session cookie + +#### GET /session +Get current session information. +- Requires: Valid session cookie +- Returns: Session details including capabilities + +### Storage Operations + +#### GET /{*path} +Read a file from storage. +- Public files: No authentication required +- Private files: Requires valid session with read capability + +#### HEAD /{*path} +Get file metadata without downloading content. +- Same authentication rules as GET + +#### PUT /{*path} +Write/update a file in storage. +- Requires: Valid session with write capability +- Body: File content (up to 100MB) + +#### DELETE /{*path} +Delete a file from storage. +- Requires: Valid session with write capability + +### Events + +#### GET /events/ +Get a feed of storage events (Server-Sent Events). +- Parameters: ?limit=N&cursor=TIMESTAMP +- Returns: Stream of file change events + +### Admin Endpoints + +These endpoints require the X-Admin-Password header. + +#### GET /generate_signup_token +Generate a new signup token (if signup mode requires tokens). + +#### POST /disable_users +Disable user accounts. + +#### GET /info +Get homeserver information and statistics. +`; + } +} diff --git a/pubky-mcp-server/src/tools.ts b/pubky-mcp-server/src/tools.ts new file mode 100644 index 0000000..253548a --- /dev/null +++ b/pubky-mcp-server/src/tools.ts @@ -0,0 +1,2591 @@ +/** + * MCP Tools for Pubky development assistance + * + * TABLE OF CONTENTS: + * ================== + * + * 1. PUBKY CORE TOOLS (lines ~810-1170) + * - get_pubky_concept: Explain Pubky concepts + * - get_code_example: Get code examples + * - search_documentation: Search docs + * - explain_capabilities: Parse capabilities + * - generate_app_scaffold: Create new app + * - get_code_template: Get templates + * - list_templates: List available templates + * + * 2. ENVIRONMENT & SETUP TOOLS (lines ~1170-1480) + * - analyze_project: Analyze existing project + * - detect_environment: Detect installed tools + * - suggest_setup: Suggest setup steps + * - ensure_dependencies: Install dependencies + * - install_pubky_testnet: Install testnet + * - setup_project_dependencies: Add Pubky deps + * - verify_installation: Verify tools + * - adapt_to_project: Generate integration code + * - integrate_pubky: Add Pubky to project + * + * 3. TESTNET TOOLS (lines ~1480-1560) + * - start_testnet: Start local testnet + * - stop_testnet: Stop testnet + * - restart_testnet: Restart testnet + * - check_testnet_status: Check if running + * - get_testnet_info: Get testnet details + * + * 4. NEXUS API TOOLS (lines ~1600-1680) + * - query_nexus_api: Search Nexus endpoints + * - explain_nexus_endpoint: Explain endpoint + * - generate_nexus_client: Generate client code + * + * 5. APP SPECS TOOLS (lines ~1680-1720) + * - generate_data_model: Generate model code + * - validate_model_data: Validate data + * - explain_model: Explain model rules + * - create_model_example: Create example + * + * 6. PKARR TOOLS (lines ~1800-2460) + * - get_pkarr_concept: Explain Pkarr concepts + * - search_pkarr_docs: Search Pkarr docs + * - get_pkarr_example: Get Pkarr examples + * - generate_pkarr_client: Generate client + * - start_pkarr_relay: Start relay + * - stop_pkarr_relay: Stop relay + * - restart_pkarr_relay: Restart relay + * - check_pkarr_relay_status: Check status + * - get_pkarr_relay_info: Get relay info + * - generate_pkarr_keypair: Generate keypair + * - generate_dns_record_builder: Build DNS records + * - explain_pkarr_key: Analyze public key + * - install_pkarr_relay: Install relay binary + * - setup_pkarr_project: Add Pkarr to project + * + * 7. PKDNS TOOLS (lines ~2460-2540) + * - get_pkdns_info: Overview and public servers + * - setup_pkdns_browser: Configure browser DNS + * - setup_pkdns_system: Configure system DNS + * - install_pkdns: Install pkdns binary + * + * 8. NEXUS IMPLEMENTATION TOOLS (lines ~2540-2600) + * - get_nexus_architecture: Architecture overview + * - setup_nexus_dev: Development environment setup + * - explain_nexus_component: Component details + */ + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import { FileReader } from './utils/file-reader.js'; +import { TestnetManager } from './utils/testnet.js'; +import { PkarrRelayManager } from './utils/pkarr-relay.js'; +import { EnvironmentDetector } from './utils/environment.js'; +import { getTemplate, listTemplates, generateScaffold, templates } from './utils/templates.js'; +import { NexusApiParser } from './utils/nexus-api.js'; +import { AppSpecsParser } from './utils/app-specs.js'; +import { + LANGUAGES, + CONCEPT_TYPES, + PROJECT_TYPES, + FRAMEWORKS, + CAPABILITY_ACTIONS, + APP_SPEC_MODELS, + PKARR_EXAMPLE_TYPES, + DEFAULT_PKARR_RELAYS, +} from './constants.js'; +import * as fs from 'fs/promises'; +import * as path from 'path'; + +// Helper functions to reduce code duplication +function createTextResponse(text: string): { content: Array<{ type: string; text: string }> } { + return { + content: [{ type: 'text', text }], + }; +} + +export class ToolHandler { + private testnetManager: TestnetManager; + private pkarrRelayManager: PkarrRelayManager | null = null; + private envDetector: EnvironmentDetector; + private nexusParser: NexusApiParser; + private specsParser: AppSpecsParser; + + constructor( + private fileReader: FileReader, + private pubkyCoreRoot: string, + private workspaceRoot: string, + private pkarrRoot?: string, + private pkdnsRoot?: string, + private nexusRoot?: string + ) { + this.testnetManager = new TestnetManager(); + if (pkarrRoot) { + this.pkarrRelayManager = new PkarrRelayManager(pkarrRoot); + } + this.envDetector = new EnvironmentDetector(); + this.nexusParser = new NexusApiParser(workspaceRoot); + this.specsParser = new AppSpecsParser(workspaceRoot); + } + + listTools(): Tool[] { + return [ + // Documentation & Learning Tools + { + name: 'get_pubky_concept', + description: + 'Get detailed explanation of a Pubky concept (homeserver, rootkey, capabilities, auth, storage, etc.)', + inputSchema: { + type: 'object', + properties: { + concept: { + type: 'string', + description: + 'The concept to explain (e.g., "homeserver", "rootkey", "capabilities", "auth", "storage")', + }, + }, + required: ['concept'], + }, + }, + { + name: 'get_code_example', + description: 'Get a specific code example from Pubky Core examples', + inputSchema: { + type: 'object', + properties: { + language: { + type: 'string', + enum: [LANGUAGES.RUST, LANGUAGES.JAVASCRIPT], + description: 'Programming language for the example', + }, + example: { + type: 'string', + description: 'Example name (e.g., "auth_flow", "storage", "signup", "testnet")', + }, + }, + required: ['language', 'example'], + }, + }, + { + name: 'search_documentation', + description: + 'Search across all Pubky documentation, examples, and code for specific topics or keywords', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'Search query or keywords', + }, + }, + required: ['query'], + }, + }, + { + name: 'explain_capabilities', + description: + 'Parse and explain Pubky capability strings, showing what permissions they grant', + inputSchema: { + type: 'object', + properties: { + capabilities: { + type: 'string', + description: 'Capability string (e.g., "/pub/my-app/:rw,/pub/shared/:r")', + }, + }, + required: ['capabilities'], + }, + }, + + // Code Generation Tools + { + name: 'generate_app_scaffold', + description: 'Generate a complete Pubky application scaffold with boilerplate code', + inputSchema: { + type: 'object', + properties: { + projectName: { + type: 'string', + description: 'Name of the project to create', + }, + language: { + type: 'string', + enum: [LANGUAGES.RUST, LANGUAGES.JAVASCRIPT, LANGUAGES.TYPESCRIPT], + description: 'Programming language to use', + }, + features: { + type: 'array', + items: { type: 'string' }, + description: 'Features to include (e.g., ["auth", "storage", "json"])', + }, + targetPath: { + type: 'string', + description: 'Path where to create the project (defaults to current directory)', + }, + }, + required: ['projectName', 'language'], + }, + }, + { + name: 'get_code_template', + description: 'Get a code template for common Pubky patterns', + inputSchema: { + type: 'object', + properties: { + templateName: { + type: 'string', + description: 'Template name (use list_templates first to see available options)', + }, + }, + required: ['templateName'], + }, + }, + { + name: 'list_templates', + description: 'List all available code templates', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + + // Environment & Project Analysis + { + name: 'analyze_project', + description: + 'Analyze the current project structure and detect language, framework, and existing dependencies', + inputSchema: { + type: 'object', + properties: { + projectPath: { + type: 'string', + description: 'Path to the project to analyze (defaults to current directory)', + }, + }, + }, + }, + { + name: 'detect_environment', + description: + 'Check what development tools are installed (Node.js, Rust, Cargo, npm, pubky-testnet)', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'suggest_setup', + description: + 'Analyze project and environment, then suggest what needs to be installed or configured', + inputSchema: { + type: 'object', + properties: { + projectPath: { + type: 'string', + description: 'Path to the project (defaults to current directory)', + }, + }, + }, + }, + + // Dependency Management + { + name: 'ensure_dependencies', + description: + 'Smart installer that checks what dependencies are needed for a Pubky project and installs missing ones', + inputSchema: { + type: 'object', + properties: { + projectPath: { + type: 'string', + description: 'Path to the project', + }, + autoInstall: { + type: 'boolean', + description: 'Automatically install without asking (default: false)', + }, + }, + required: ['projectPath'], + }, + }, + { + name: 'install_pubky_testnet', + description: 'Install the pubky-testnet binary using cargo', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'setup_project_dependencies', + description: + 'Add Pubky dependencies to an existing project (updates package.json or Cargo.toml)', + inputSchema: { + type: 'object', + properties: { + projectPath: { + type: 'string', + description: 'Path to the project', + }, + }, + required: ['projectPath'], + }, + }, + { + name: 'verify_installation', + description: + 'Verify that all required tools for Pubky development are properly installed and working', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + + // Testnet Management + { + name: 'start_testnet', + description: 'Start a local Pubky testnet (DHT + homeserver + relay) for development', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'stop_testnet', + description: 'Stop the running local testnet', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'restart_testnet', + description: 'Restart the local testnet (useful after code changes)', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'check_testnet_status', + description: 'Check if the local testnet is running and responsive', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'get_testnet_info', + description: 'Get detailed information about the testnet (public key, ports, URLs)', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + + // Pkarr Tools (Discovery Layer) + { + name: 'get_pkarr_concept', + description: + 'Explain Pkarr concepts (discovery, DHT, relays, signed packets, DNS records, republishing, keypairs)', + inputSchema: { + type: 'object', + properties: { + concept: { + type: 'string', + description: + 'Concept to explain (e.g., "discovery", "dht", "relay", "signed-packet", "dns-records")', + }, + }, + required: ['concept'], + }, + }, + { + name: 'search_pkarr_docs', + description: 'Search Pkarr design docs and examples for specific topics', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'Search query or keywords', + }, + }, + required: ['query'], + }, + }, + { + name: 'get_pkarr_example', + description: + 'Get Pkarr code examples (publish, resolve, http-serve, http-get) in Rust or JavaScript', + inputSchema: { + type: 'object', + properties: { + language: { + type: 'string', + enum: [LANGUAGES.RUST, LANGUAGES.JAVASCRIPT], + description: 'Programming language', + }, + example: { + type: 'string', + enum: [ + PKARR_EXAMPLE_TYPES.PUBLISH, + PKARR_EXAMPLE_TYPES.RESOLVE, + PKARR_EXAMPLE_TYPES.HTTP_SERVE, + PKARR_EXAMPLE_TYPES.HTTP_GET, + ], + description: 'Example type', + }, + }, + required: ['language', 'example'], + }, + }, + { + name: 'generate_pkarr_client', + description: 'Generate Pkarr client code for publishing/resolving records', + inputSchema: { + type: 'object', + properties: { + language: { + type: 'string', + enum: [LANGUAGES.RUST, LANGUAGES.JAVASCRIPT, LANGUAGES.TYPESCRIPT], + description: 'Programming language', + }, + includePublish: { + type: 'boolean', + description: 'Include publishing functionality', + }, + includeResolve: { + type: 'boolean', + description: 'Include resolving functionality', + }, + }, + required: ['language'], + }, + }, + { + name: 'start_pkarr_relay', + description: 'Start local Pkarr relay for development', + inputSchema: { + type: 'object', + properties: { + port: { + type: 'number', + description: 'Port number (default: 6881)', + }, + testnet: { + type: 'boolean', + description: 'Run in testnet mode', + }, + }, + }, + }, + { + name: 'stop_pkarr_relay', + description: 'Stop running Pkarr relay', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'restart_pkarr_relay', + description: 'Restart Pkarr relay with new config', + inputSchema: { + type: 'object', + properties: { + port: { + type: 'number', + description: 'Port number (default: 6881)', + }, + testnet: { + type: 'boolean', + description: 'Run in testnet mode', + }, + }, + }, + }, + { + name: 'check_pkarr_relay_status', + description: 'Check if Pkarr relay is running', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'get_pkarr_relay_info', + description: 'Get Pkarr relay details (URL, port, cache info)', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'generate_pkarr_keypair', + description: 'Generate code to create/manage Pkarr keypairs', + inputSchema: { + type: 'object', + properties: { + language: { + type: 'string', + enum: [LANGUAGES.RUST, LANGUAGES.JAVASCRIPT, LANGUAGES.TYPESCRIPT], + description: 'Programming language', + }, + }, + required: ['language'], + }, + }, + { + name: 'generate_dns_record_builder', + description: + 'Generate code to build DNS records (A, AAAA, TXT, CNAME, NS, HTTPS, SVCB) for Pkarr', + inputSchema: { + type: 'object', + properties: { + language: { + type: 'string', + enum: [LANGUAGES.RUST, LANGUAGES.JAVASCRIPT, LANGUAGES.TYPESCRIPT], + description: 'Programming language', + }, + recordTypes: { + type: 'array', + items: { type: 'string' }, + description: 'DNS record types to include (e.g., ["A", "TXT", "HTTPS"])', + }, + }, + required: ['language'], + }, + }, + { + name: 'explain_pkarr_key', + description: 'Parse and explain a z-base32 Pkarr public key', + inputSchema: { + type: 'object', + properties: { + publicKey: { + type: 'string', + description: 'Z-base32 encoded public key', + }, + }, + required: ['publicKey'], + }, + }, + { + name: 'install_pkarr_relay', + description: 'Install pkarr-relay binary via cargo', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'setup_pkarr_project', + description: 'Add Pkarr dependencies to existing project', + inputSchema: { + type: 'object', + properties: { + projectPath: { + type: 'string', + description: 'Path to the project', + }, + }, + required: ['projectPath'], + }, + }, + + // Pkdns Tools (DNS Resolver) + { + name: 'get_pkdns_info', + description: 'Get pkdns overview and how it resolves Pkarr domains as TLDs', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'setup_pkdns_browser', + description: 'Guide to configure browser to use pkdns DNS-over-HTTPS', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'setup_pkdns_system', + description: 'Guide to configure system DNS to use pkdns', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'install_pkdns', + description: 'Install pkdns binary via cargo', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + + // Nexus Implementation Tools (for developers) + { + name: 'get_nexus_architecture', + description: 'Understand Nexus architecture: watcher, service, databases', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'setup_nexus_dev', + description: 'Set up Nexus development environment (Neo4j, Redis, Docker)', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'explain_nexus_component', + description: 'Explain a specific Nexus component (watcher, service, common)', + inputSchema: { + type: 'object', + properties: { + component: { + type: 'string', + enum: ['watcher', 'service', 'common', 'nexusd'], + description: 'Component to explain', + }, + }, + required: ['component'], + }, + }, + + // Integration & Debugging + { + name: 'adapt_to_project', + description: + 'Analyze existing project and generate Pubky integration code that matches the project structure and patterns', + inputSchema: { + type: 'object', + properties: { + projectPath: { + type: 'string', + description: 'Path to the project', + }, + }, + required: ['projectPath'], + }, + }, + { + name: 'integrate_pubky', + description: + 'Add Pubky to an existing project (detects framework, adds dependencies, generates appropriate code)', + inputSchema: { + type: 'object', + properties: { + projectPath: { + type: 'string', + description: 'Path to the project', + }, + }, + required: ['projectPath'], + }, + }, + + // Nexus API Tools (for READING social data) + { + name: 'query_nexus_api', + description: 'Search and query Nexus API endpoints by keyword or category. Nexus is where you READ social data (feeds, search, discovery). It crawls all homeservers and indexes public data.', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'Search term or category (posts, users, streams, search, tags)', + }, + }, + required: ['query'], + }, + }, + { + name: 'explain_nexus_endpoint', + description: 'Get detailed explanation and example for a specific Nexus API endpoint. Use Nexus to read aggregated social data, not to write posts (write to your homeserver instead).', + inputSchema: { + type: 'object', + properties: { + operationId: { + type: 'string', + description: + 'Operation ID of the endpoint (e.g., "post_view_handler", "stream_posts_handler")', + }, + }, + required: ['operationId'], + }, + }, + { + name: 'generate_nexus_client', + description: 'Generate API client code for reading social data from Nexus (feeds, search, user discovery). Note: To write posts/profiles, use homeserver storage tools instead.', + inputSchema: { + type: 'object', + properties: { + language: { + type: 'string', + enum: [LANGUAGES.JAVASCRIPT, LANGUAGES.TYPESCRIPT, LANGUAGES.RUST], + description: 'Programming language for the client', + }, + endpoints: { + type: 'array', + items: { type: 'string' }, + description: + 'List of endpoint operation IDs to include (leave empty for common endpoints)', + }, + }, + required: ['language'], + }, + }, + + // Pubky App Specs Tools (data format for WRITING to homeserver) + { + name: 'generate_data_model', + description: 'Generate code for Pubky app data models with validation. Use this format when WRITING to YOUR homeserver. Nexus will automatically index it if stored in /pub/pubky.app/*', + inputSchema: { + type: 'object', + properties: { + model: { + type: 'string', + enum: Object.values(APP_SPEC_MODELS), + description: 'Model name (user, post, tag, bookmark, follow, file, feed)', + }, + language: { + type: 'string', + enum: [LANGUAGES.JAVASCRIPT, LANGUAGES.TYPESCRIPT, LANGUAGES.RUST], + description: 'Programming language', + }, + }, + required: ['model', 'language'], + }, + }, + { + name: 'validate_model_data', + description: 'Validate data against Pubky app model specifications. Ensures your data follows the interoperability contract before writing to homeserver.', + inputSchema: { + type: 'object', + properties: { + model: { + type: 'string', + enum: Object.values(APP_SPEC_MODELS), + description: 'Model name to validate against', + }, + data: { + type: 'object', + description: 'Data object to validate', + }, + }, + required: ['model', 'data'], + }, + }, + { + name: 'explain_model', + description: 'Get detailed explanation of a Pubky app data model. Understand the schema and validation rules for interoperable social data.', + inputSchema: { + type: 'object', + properties: { + model: { + type: 'string', + enum: Object.values(APP_SPEC_MODELS), + description: 'Model name', + }, + }, + required: ['model'], + }, + }, + { + name: 'create_model_example', + description: 'Create example data for a Pubky app model. Shows how to properly format data before writing to your homeserver.', + inputSchema: { + type: 'object', + properties: { + model: { + type: 'string', + enum: Object.values(APP_SPEC_MODELS), + description: 'Model name', + }, + language: { + type: 'string', + enum: [LANGUAGES.JAVASCRIPT, LANGUAGES.TYPESCRIPT, LANGUAGES.RUST], + description: 'Programming language', + }, + }, + required: ['model', 'language'], + }, + }, + ]; + } + + async executeTool( + name: string, + args: any + ): Promise<{ content: Array<{ type: string; text: string }> }> { + try { + switch (name) { + // Documentation & Learning + case 'get_pubky_concept': + return await this.getPubkyConcept(args.concept); + case 'get_code_example': + return await this.getCodeExample(args.language, args.example); + case 'search_documentation': + return await this.searchDocumentation(args.query); + case 'explain_capabilities': + return await this.explainCapabilities(args.capabilities); + + // Code Generation + case 'generate_app_scaffold': + return await this.generateAppScaffold( + args.projectName, + args.language, + args.features || [], + args.targetPath + ); + case 'get_code_template': + return await this.getCodeTemplate(args.templateName); + case 'list_templates': + return await this.listCodeTemplates(); + + // Environment & Analysis + case 'analyze_project': + return await this.analyzeProject(args.projectPath || process.cwd()); + case 'detect_environment': + return await this.detectEnvironment(); + case 'suggest_setup': + return await this.suggestSetup(args.projectPath || process.cwd()); + + // Dependency Management + case 'ensure_dependencies': + return await this.ensureDependencies(args.projectPath, args.autoInstall); + case 'install_pubky_testnet': + return await this.installPubkyTestnet(); + case 'setup_project_dependencies': + return await this.setupProjectDependencies(args.projectPath); + case 'verify_installation': + return await this.verifyInstallation(); + + // Testnet Management + case 'start_testnet': + return await this.startTestnet(); + case 'stop_testnet': + return await this.stopTestnet(); + case 'restart_testnet': + return await this.restartTestnet(); + case 'check_testnet_status': + return await this.checkTestnetStatus(); + case 'get_testnet_info': + return await this.getTestnetInfo(); + + // Pkarr Tools + case 'get_pkarr_concept': + return await this.getPkarrConcept(args.concept); + case 'search_pkarr_docs': + return await this.searchPkarrDocs(args.query); + case 'get_pkarr_example': + return await this.getPkarrExample(args.language, args.example); + case 'generate_pkarr_client': + return await this.generatePkarrClient( + args.language, + args.includePublish, + args.includeResolve + ); + case 'start_pkarr_relay': + return await this.startPkarrRelay(args.port, args.testnet); + case 'stop_pkarr_relay': + return await this.stopPkarrRelay(); + case 'restart_pkarr_relay': + return await this.restartPkarrRelay(args.port, args.testnet); + case 'check_pkarr_relay_status': + return await this.checkPkarrRelayStatus(); + case 'get_pkarr_relay_info': + return await this.getPkarrRelayInfo(); + case 'generate_pkarr_keypair': + return await this.generatePkarrKeypair(args.language); + case 'generate_dns_record_builder': + return await this.generateDnsRecordBuilder(args.language, args.recordTypes); + case 'explain_pkarr_key': + return await this.explainPkarrKey(args.publicKey); + case 'install_pkarr_relay': + return await this.installPkarrRelay(); + case 'setup_pkarr_project': + return await this.setupPkarrProject(args.projectPath); + + // Pkdns Tools + case 'get_pkdns_info': + return await this.getPkdnsInfo(); + case 'setup_pkdns_browser': + return await this.setupPkdnsBrowser(); + case 'setup_pkdns_system': + return await this.setupPkdnsSystem(); + case 'install_pkdns': + return await this.installPkdns(); + + // Nexus Implementation Tools + case 'get_nexus_architecture': + return await this.getNexusArchitecture(); + case 'setup_nexus_dev': + return await this.setupNexusDev(); + case 'explain_nexus_component': + return await this.explainNexusComponent(args.component); + + // Integration + case 'adapt_to_project': + return await this.adaptToProject(args.projectPath); + case 'integrate_pubky': + return await this.integratePubky(args.projectPath); + + // Nexus API + case 'query_nexus_api': + return await this.queryNexusApi(args.query); + case 'explain_nexus_endpoint': + return await this.explainNexusEndpoint(args.operationId); + case 'generate_nexus_client': + return await this.generateNexusClient(args.language, args.endpoints || []); + + // Pubky App Specs + case 'generate_data_model': + return await this.generateDataModel(args.model, args.language); + case 'validate_model_data': + return await this.validateModelData(args.model, args.data); + case 'explain_model': + return await this.explainModel(args.model); + case 'create_model_example': + return await this.createModelExample(args.model, args.language); + + default: + throw new Error(`Unknown tool: ${name}`); + } + } catch (error: any) { + return { + content: [ + { + type: 'text', + text: `Error executing ${name}: ${error.message}`, + }, + ], + }; + } + } + + // ============================================================================ + // 1. PUBKY CORE TOOLS + // ============================================================================ + // Concepts, examples, auth, storage, capabilities + + private async getPubkyConcept( + concept: string + ): Promise<{ content: Array<{ type: string; text: string }> }> { + const conceptLower = concept.toLowerCase(); + + try { + switch (true) { + case conceptLower.includes(CONCEPT_TYPES.HOMESERVER): { + const content = await this.fileReader.readDocFile('src/concepts/homeserver.md'); + const readme = await this.fileReader.readFile( + path.join(this.pubkyCoreRoot, 'pubky-homeserver/README.md') + ); + return { + content: [ + { + type: 'text', + text: `# Homeserver Concept\n\n${content}\n\n---\n\n# Homeserver Implementation\n\n${readme}`, + }, + ], + }; + } + case conceptLower.includes('root') || conceptLower.includes('key'): { + const content = await this.fileReader.readDocFile('src/concepts/rootkey.md'); + return { + content: [{ type: 'text', text: content }], + }; + } + case conceptLower.includes(CONCEPT_TYPES.AUTH) || conceptLower.includes('authentication'): { + const content = await this.fileReader.readDocFile('src/spec/auth.md'); + return { + content: [{ type: 'text', text: content }], + }; + } + case conceptLower.includes(CONCEPT_TYPES.CAPABILITIES): { + const capFile = await this.fileReader.readFile( + path.join(this.pubkyCoreRoot, 'pubky-common/src/capabilities.rs') + ); + return { + content: [ + { + type: 'text', + text: `# Capabilities\n\nCapabilities define what a session can access and how.\n\n## Implementation\n\n\`\`\`rust\n${capFile}\n\`\`\``, + }, + ], + }; + } + case conceptLower.includes(CONCEPT_TYPES.STORAGE): { + const sdkReadme = await this.fileReader.readFile( + path.join(this.pubkyCoreRoot, 'pubky-sdk/README.md') + ); + return { + content: [ + { + type: 'text', + text: `# Storage\n\nPubky provides a key-value storage API through HTTP.\n\n${sdkReadme}`, + }, + ], + }; + } + default: + // General search + return await this.searchDocumentation(concept); + } + } catch { + return createTextResponse( + `Could not find information about "${concept}". Try: homeserver, rootkey, auth, capabilities, or storage.` + ); + } + } + + private async getCodeExample( + language: 'rust' | 'javascript', + example: string + ): Promise<{ content: Array<{ type: string; text: string }> }> { + const examplePath = + language === 'rust' + ? this.fileReader.getPaths().examplesRust + : this.fileReader.getPaths().examplesJs; + + // Map common names to actual directories + const exampleMap: Record = { + auth: '3-auth_flow', + auth_flow: '3-auth_flow', + authentication: '3-auth_flow', + storage: '4-storage', + signup: '2-signup', + testnet: '1-testnet', + logging: '0-logging', + request: '5-request', + }; + + const actualExample = exampleMap[example.toLowerCase()] || example; + + try { + const readme = await this.fileReader.readFile( + path.join(examplePath, actualExample, 'README.md') + ); + + // Try to get code files + let code = ''; + try { + if (language === 'rust') { + const mainFile = await this.fileReader.readFile( + path.join(examplePath, actualExample, 'main.rs') + ); + code = `\n\n## Code (main.rs)\n\n\`\`\`rust\n${mainFile}\n\`\`\``; + } else { + const files = await this.fileReader.listDirectory(path.join(examplePath, actualExample)); + const jsFile = files.find(f => f.endsWith('.mjs') || f.endsWith('.js')); + if (jsFile) { + const content = await this.fileReader.readFile( + path.join(examplePath, actualExample, jsFile) + ); + code = `\n\n## Code (${jsFile})\n\n\`\`\`javascript\n${content}\n\`\`\``; + } + } + } catch { + // No code file found + } + + return { + content: [ + { + type: 'text', + text: `${readme}${code}`, + }, + ], + }; + } catch { + return createTextResponse( + `Could not find example "${example}". Available examples: auth_flow, storage, signup, testnet, logging, request` + ); + } + } + + private async searchDocumentation( + query: string + ): Promise<{ content: Array<{ type: string; text: string }> }> { + const docsPath = this.fileReader.getPaths().docs; + const examplesPath = this.fileReader.getPaths().examples; + + const results = await this.fileReader.searchFiles(docsPath, query); + const exampleResults = await this.fileReader.searchFiles(examplesPath, query); + + // Also search Nexus API and App Specs + let nexusResults = ''; + let specsResults = ''; + + try { + nexusResults = await this.nexusParser.searchEndpoints(query); + } catch { + // Nexus search failed, skip + } + + try { + specsResults = await this.specsParser.searchModels(query); + } catch { + // Specs search failed, skip + } + + let output = `# Search Results for "${query}"\n\n`; + + let foundAny = results.length > 0 || exampleResults.length > 0 || nexusResults.length > 0 || specsResults.length > 0; + + if (!foundAny) { + output += 'No results found.'; + } else { + if (results.length > 0) { + output += `## Pubky Core Documentation (${results.length} matches)\n\n`; + for (const result of results.slice(0, 10)) { + output += `### ${result.path}\n\n`; + for (const match of result.matches) { + output += `- ${match}\n`; + } + output += '\n'; + } + } + + if (exampleResults.length > 0) { + output += `\n## Pubky Core Examples (${exampleResults.length} matches)\n\n`; + for (const result of exampleResults.slice(0, 10)) { + output += `### ${result.path}\n\n`; + for (const match of result.matches) { + output += `- ${match}\n`; + } + output += '\n'; + } + } + + if (nexusResults && !nexusResults.includes('No endpoints found')) { + output += `\n## Nexus API\n\n${nexusResults}\n\n`; + } + + if (specsResults && !specsResults.includes('No matches found')) { + output += `\n## Pubky App Specs\n\n${specsResults}\n\n`; + } + } + + return { + content: [{ type: 'text', text: output }], + }; + } + + private async explainCapabilities( + capabilities: string + ): Promise<{ content: Array<{ type: string; text: string }> }> { + const caps = capabilities.split(',').map(c => c.trim()); + + let explanation = `# Capabilities Explanation\n\nYou provided: \`${capabilities}\`\n\n`; + + for (const cap of caps) { + const [scope, actions] = cap.split(':'); + explanation += `## \`${cap}\`\n\n`; + explanation += `- **Scope**: \`${scope}\`\n`; + explanation += `- **Actions**: \`${actions || 'none'}\`\n\n`; + + if (actions) { + if (actions.includes('r')) { + explanation += `- βœ“ **Read** (GET): Can read files in ${scope}\n`; + } + if (actions.includes('w')) { + explanation += `- βœ“ **Write** (PUT/POST/DELETE): Can write/modify/delete files in ${scope}\n`; + } + } + + explanation += `\n`; + + // Add best practices + if (scope === '/') { + explanation += `⚠️ **Warning**: This grants access to the entire storage! Consider being more specific.\n\n`; + } else if (!scope.startsWith('/pub/')) { + explanation += `⚠️ **Note**: Non-public paths (not under /pub/) may have restricted access.\n\n`; + } + } + + explanation += `\n## How to use in code\n\n`; + explanation += `### JavaScript\n\`\`\`javascript\nimport { Capabilities } from '@synonymdev/pubky';\n\n`; + explanation += `const caps = Capabilities.builder()\n`; + for (const cap of caps) { + const [scope, actions] = cap.split(':'); + switch (true) { + case actions === CAPABILITY_ACTIONS.READ_WRITE: + explanation += ` .readWrite('${scope}')\n`; + break; + case actions === CAPABILITY_ACTIONS.READ: + explanation += ` .read('${scope}')\n`; + break; + case actions === CAPABILITY_ACTIONS.WRITE: + explanation += ` .write('${scope}')\n`; + break; + } + } + explanation += ` .finish();\n\`\`\`\n\n`; + + explanation += `### Rust\n\`\`\`rust\nuse pubky::prelude::*;\n\n`; + explanation += `let caps = Capabilities::builder()\n`; + for (const cap of caps) { + const [scope, actions] = cap.split(':'); + switch (true) { + case actions === CAPABILITY_ACTIONS.READ_WRITE: + explanation += ` .read_write("${scope}")\n`; + break; + case actions === CAPABILITY_ACTIONS.READ: + explanation += ` .read("${scope}")\n`; + break; + case actions === CAPABILITY_ACTIONS.WRITE: + explanation += ` .write("${scope}")\n`; + break; + } + } + explanation += ` .finish();\n\`\`\`\n`; + + return { + content: [{ type: 'text', text: explanation }], + }; + } + + private async generateAppScaffold( + projectName: string, + language: 'rust' | 'javascript' | 'typescript', + features: string[], + targetPath?: string + ): Promise<{ content: Array<{ type: string; text: string }> }> { + const scaffold = generateScaffold(projectName, language, features); + const projectPath = path.join(targetPath || process.cwd(), projectName); + + try { + // Create project directory + await fs.mkdir(projectPath, { recursive: true }); + + // Create all files + for (const [filePath, content] of scaffold.files) { + const fullPath = path.join(projectPath, filePath); + await fs.mkdir(path.dirname(fullPath), { recursive: true }); + await fs.writeFile(fullPath, content, 'utf-8'); + } + + return { + content: [ + { + type: 'text', + text: `βœ… Project scaffold created successfully!\n\n${scaffold.instructions}`, + }, + ], + }; + } catch (error: any) { + return { + content: [ + { + type: 'text', + text: `Error creating scaffold: ${error.message}`, + }, + ], + }; + } + } + + private async getCodeTemplate( + templateName: string + ): Promise<{ content: Array<{ type: string; text: string }> }> { + const template = getTemplate(templateName); + + if (!template) { + const available = listTemplates(); + return { + content: [ + { + type: 'text', + text: `Template "${templateName}" not found.\n\nAvailable templates:\n${available.map(t => `- ${t.name}: ${t.description}`).join('\n')}`, + }, + ], + }; + } + + let output = `# ${template.name}\n\n${template.description}\n\n`; + output += `**Language**: ${template.language}\n\n`; + + if (template.dependencies && template.dependencies.length > 0) { + output += `**Dependencies**:\n${template.dependencies.map(d => `- ${d}`).join('\n')}\n\n`; + } + + output += `## Code\n\n\`\`\`${template.language}\n${template.code}\n\`\`\``; + + return { + content: [{ type: 'text', text: output }], + }; + } + + private async listCodeTemplates(): Promise<{ content: Array<{ type: string; text: string }> }> { + const templates = listTemplates(); + + let output = '# Available Code Templates\n\n'; + + const byLanguage: Record = {}; + for (const template of templates) { + if (!byLanguage[template.language]) { + byLanguage[template.language] = []; + } + byLanguage[template.language].push(template); + } + + for (const [language, temps] of Object.entries(byLanguage)) { + output += `## ${language.charAt(0).toUpperCase() + language.slice(1)}\n\n`; + for (const template of temps) { + output += `- **${template.name}**: ${template.description}\n`; + } + output += '\n'; + } + + return { + content: [{ type: 'text', text: output }], + }; + } + + // ============================================================================ + // 2. ENVIRONMENT & SETUP TOOLS + // ============================================================================ + // Project analysis, dependency management, installation + + private async analyzeProject( + projectPath: string + ): Promise<{ content: Array<{ type: string; text: string }> }> { + const analysis = await this.envDetector.analyzeProject(projectPath); + + let output = `# Project Analysis\n\n`; + output += `**Path**: ${projectPath}\n`; + output += `**Type**: ${analysis.type}\n`; + output += `**Has Pubky**: ${analysis.hasPubkyDependency ? 'βœ… Yes' : '❌ No'}\n\n`; + + if (analysis.framework) { + output += `**Framework**: ${analysis.framework}\n\n`; + } + + if (analysis.dependencies.length > 0) { + output += `**Dependencies** (${analysis.dependencies.length}):\n`; + for (const dep of analysis.dependencies.slice(0, 10)) { + output += `- ${dep}\n`; + } + if (analysis.dependencies.length > 10) { + output += `- ... and ${analysis.dependencies.length - 10} more\n`; + } + output += '\n'; + } + + return { + content: [{ type: 'text', text: output }], + }; + } + + private async detectEnvironment(): Promise<{ content: Array<{ type: string; text: string }> }> { + const env = await this.envDetector.detect(); + + let output = `# Environment Detection\n\n`; + output += `**OS**: ${env.os}\n`; + output += `**Architecture**: ${env.arch}\n\n`; + + output += `## Installed Tools\n\n`; + + if (env.node) { + output += `βœ… **Node.js**: ${env.node.version} (${env.node.path})\n`; + } else { + output += `❌ **Node.js**: Not installed\n`; + } + + if (env.npm) { + output += `βœ… **npm**: ${env.npm.version} (${env.npm.path})\n`; + } else { + output += `❌ **npm**: Not installed\n`; + } + + if (env.rust) { + output += `βœ… **Rust**: ${env.rust.version} (${env.rust.path})\n`; + } else { + output += `❌ **Rust**: Not installed\n`; + } + + if (env.cargo) { + output += `βœ… **Cargo**: ${env.cargo.version} (${env.cargo.path})\n`; + } else { + output += `❌ **Cargo**: Not installed\n`; + } + + if (env.pubkyTestnet?.installed) { + output += `βœ… **pubky-testnet**: Installed (${env.pubkyTestnet.path})\n`; + } else { + output += `❌ **pubky-testnet**: Not installed\n`; + } + + return { + content: [{ type: 'text', text: output }], + }; + } + + private async suggestSetup( + projectPath: string + ): Promise<{ content: Array<{ type: string; text: string }> }> { + const env = await this.envDetector.detect(); + const analysis = await this.envDetector.analyzeProject(projectPath); + + let output = `# Setup Suggestions\n\n`; + + const missing: string[] = []; + const suggestions: string[] = []; + + // Check based on project type + switch (true) { + case analysis.type === PROJECT_TYPES.RUST: { + if (!env.cargo) { + missing.push('Rust/Cargo'); + suggestions.push('Install Rust: https://rustup.rs/'); + } + if (!analysis.hasPubkyDependency) { + suggestions.push('Add Pubky to Cargo.toml: `pubky = "0.4"`'); + } + break; + } + case analysis.type === PROJECT_TYPES.JAVASCRIPT || + analysis.type === PROJECT_TYPES.TYPESCRIPT: { + if (!env.node) { + missing.push('Node.js'); + suggestions.push('Install Node.js: https://nodejs.org/'); + } + if (!env.npm) { + missing.push('npm'); + suggestions.push('Install npm (usually comes with Node.js)'); + } + if (!analysis.hasPubkyDependency) { + suggestions.push('Install Pubky SDK: `npm install @synonymdev/pubky`'); + } + break; + } + } + + // Testnet recommendation + if (!env.pubkyTestnet?.installed) { + suggestions.push( + 'Install pubky-testnet for local development: `cargo install pubky-testnet`' + ); + } + + if (missing.length > 0) { + output += `## ❌ Missing Required Tools\n\n`; + for (const tool of missing) { + output += `- ${tool}\n`; + } + output += '\n'; + } + + if (suggestions.length > 0) { + output += `## πŸ’‘ Recommendations\n\n`; + for (const suggestion of suggestions) { + output += `- ${suggestion}\n`; + } + } else { + output += `βœ… Everything looks good! You're ready to develop with Pubky.\n`; + } + + return { + content: [{ type: 'text', text: output }], + }; + } + + private async ensureDependencies( + projectPath: string, + autoInstall: boolean = false + ): Promise<{ content: Array<{ type: string; text: string }> }> { + const analysis = await this.envDetector.analyzeProject(projectPath); + + let output = `# Ensuring Dependencies\n\n`; + + if (analysis.hasPubkyDependency) { + output += `βœ… Pubky is already installed in this project.\n`; + return { content: [{ type: 'text', text: output }] }; + } + + if (!autoInstall) { + output += `Pubky is not installed. Use \`setup_project_dependencies\` to add it.\n`; + return { content: [{ type: 'text', text: output }] }; + } + + try { + switch (true) { + case analysis.type === PROJECT_TYPES.RUST: { + await this.envDetector.addCargodependency(projectPath); + output += `βœ… Added pubky to Cargo.toml\n`; + break; + } + case analysis.hasPackageJson: { + const result = await this.envDetector.installNpmPackage(projectPath); + output += `βœ… Installed @synonymdev/pubky\n\n${result}\n`; + break; + } + } + } catch (error: any) { + output += `❌ Error: ${error.message}\n`; + } + + return { + content: [{ type: 'text', text: output }], + }; + } + + private async installPubkyTestnet(): Promise<{ content: Array<{ type: string; text: string }> }> { + try { + const result = await this.envDetector.installPubkyTestnet(); + return { + content: [ + { + type: 'text', + text: `βœ… Successfully installed pubky-testnet!\n\n${result}\n\nYou can now run it with: \`pubky-testnet\``, + }, + ], + }; + } catch (error: any) { + return { + content: [ + { + type: 'text', + text: `❌ Failed to install pubky-testnet: ${error.message}\n\nMake sure you have Rust and Cargo installed.`, + }, + ], + }; + } + } + + private async setupProjectDependencies( + projectPath: string + ): Promise<{ content: Array<{ type: string; text: string }> }> { + const analysis = await this.envDetector.analyzeProject(projectPath); + + let output = `# Setting up Pubky Dependencies\n\n`; + + try { + switch (true) { + case analysis.type === PROJECT_TYPES.RUST: { + if (analysis.hasCargoToml) { + await this.envDetector.addCargodependency(projectPath); + output += `βœ… Added pubky to Cargo.toml\n\nRun \`cargo build\` to download the dependency.`; + } else { + output += `❌ No Cargo.toml found. This doesn't appear to be a Rust project.`; + } + break; + } + case analysis.type === PROJECT_TYPES.JAVASCRIPT || + analysis.type === PROJECT_TYPES.TYPESCRIPT: { + if (analysis.hasPackageJson) { + const result = await this.envDetector.installNpmPackage(projectPath); + output += `βœ… Installed @synonymdev/pubky\n\n${result}`; + } else { + output += `❌ No package.json found. Run \`npm init\` first.`; + } + break; + } + default: + output += `❌ Unknown project type. Cannot determine how to add dependencies.`; + } + } catch (error: any) { + output += `❌ Error: ${error.message}`; + } + + return { + content: [{ type: 'text', text: output }], + }; + } + + private async verifyInstallation(): Promise<{ content: Array<{ type: string; text: string }> }> { + const env = await this.envDetector.detect(); + + let output = `# Installation Verification\n\n`; + let allGood = true; + + const checks = [ + { name: 'Node.js', installed: !!env.node, required: false }, + { name: 'npm', installed: !!env.npm, required: false }, + { name: 'Rust', installed: !!env.rust, required: false }, + { name: 'Cargo', installed: !!env.cargo, required: false }, + { name: 'pubky-testnet', installed: env.pubkyTestnet?.installed || false, required: false }, + ]; + + for (const check of checks) { + if (check.installed) { + output += `βœ… ${check.name}: Installed\n`; + } else { + output += `${check.required ? '❌' : '⚠️'} ${check.name}: Not installed${check.required ? ' (required)' : ''}\n`; + if (check.required) allGood = false; + } + } + + output += `\n`; + + if (allGood) { + output += `βœ… All checks passed! You're ready for Pubky development.\n`; + } else { + output += `⚠️ Some tools are missing. Install them based on your needs:\n`; + output += `- For JavaScript/TypeScript: Install Node.js and npm\n`; + output += `- For Rust: Install Rust and Cargo from https://rustup.rs/\n`; + output += `- For local testing: Install pubky-testnet with \`cargo install pubky-testnet\`\n`; + } + + return { + content: [{ type: 'text', text: output }], + }; + } + + // ============================================================================ + // 3. TESTNET TOOLS + // ============================================================================ + // Local development testnet management + + private async startTestnet(): Promise<{ content: Array<{ type: string; text: string }> }> { + try { + const result = await this.testnetManager.start(this.pubkyCoreRoot); + return createTextResponse(result); + } catch (error: any) { + return createTextResponse( + `Failed to start testnet: ${error.message}\n\nMake sure pubky-testnet is installed or run from the pubky-core directory.` + ); + } + } + + private async stopTestnet(): Promise<{ content: Array<{ type: string; text: string }> }> { + const result = await this.testnetManager.stop(); + return createTextResponse(result); + } + + private async restartTestnet(): Promise<{ content: Array<{ type: string; text: string }> }> { + const result = await this.testnetManager.restart(this.pubkyCoreRoot); + return createTextResponse(result); + } + + private async checkTestnetStatus(): Promise<{ content: Array<{ type: string; text: string }> }> { + const isRunning = await this.testnetManager.isRunning(); + const status = isRunning ? 'βœ… Testnet is running' : '❌ Testnet is not running'; + + if (isRunning) { + const info = await this.testnetManager.getInfo(); + return { + content: [ + { + type: 'text', + text: `${status}\n\nHomeserver: ${info.urls.homeserver}\nPublic Key: ${info.homeserverPublicKey}`, + }, + ], + }; + } + + return { + content: [ + { + type: 'text', + text: `${status}\n\nStart it with the \`start_testnet\` tool.`, + }, + ], + }; + } + + private async getTestnetInfo(): Promise<{ content: Array<{ type: string; text: string }> }> { + const info = await this.testnetManager.getInfo(); + + let output = `# Testnet Information\n\n`; + output += `**Status**: ${info.isRunning ? 'βœ… Running' : '❌ Not running'}\n\n`; + output += `**Homeserver Public Key**: \`${info.homeserverPublicKey}\`\n\n`; + output += `## Ports\n\n`; + output += `- DHT: ${info.ports.dht}\n`; + output += `- Pkarr Relay: ${info.ports.pkarrRelay}\n`; + output += `- HTTP Relay: ${info.ports.httpRelay}\n`; + output += `- Admin Server: ${info.ports.adminServer}\n\n`; + output += `## URLs\n\n`; + output += `- Homeserver: ${info.urls.homeserver}\n`; + output += `- HTTP Relay: ${info.urls.httpRelay}\n`; + output += `- Admin: ${info.urls.admin}\n\n`; + output += `## Usage in Code\n\n`; + output += `### JavaScript\n\`\`\`javascript\nconst pubky = Pubky.testnet();\n\`\`\`\n\n`; + output += `### Rust\n\`\`\`rust\nlet pubky = Pubky::testnet()?;\n\`\`\``; + + return { + content: [{ type: 'text', text: output }], + }; + } + + private async adaptToProject( + projectPath: string + ): Promise<{ content: Array<{ type: string; text: string }> }> { + const analysis = await this.envDetector.analyzeProject(projectPath); + + let output = `# Pubky Integration for Your Project\n\n`; + output += `Based on your ${analysis.type} project${analysis.framework ? ` using ${analysis.framework}` : ''}, here's how to integrate Pubky:\n\n`; + + switch (true) { + case analysis.type === PROJECT_TYPES.RUST: { + output += `## 1. Add Dependency\n\nAdd to your Cargo.toml:\n\`\`\`toml\n[dependencies]\npubky = "0.4"\ntokio = { version = "1", features = ["full"] }\n\`\`\`\n\n`; + output += `## 2. Basic Integration\n\n\`\`\`rust\n${templates['rust-basic-app'].code}\n\`\`\`\n\n`; + break; + } + case analysis.framework === FRAMEWORKS.REACT: { + output += `## 1. Install Package\n\n\`\`\`bash\nnpm install @synonymdev/pubky\n\`\`\`\n\n`; + output += `## 2. Create Pubky Context\n\n\`\`\`typescript\nimport { createContext, useContext, useState } from 'react';\nimport { Pubky, PubkySession } from '@synonymdev/pubky';\n\nconst PubkyContext = createContext<{ pubky: Pubky; session: PubkySession | null }>({ pubky: new Pubky(), session: null });\n\nexport function PubkyProvider({ children }: { children: React.ReactNode }) {\n const [pubky] = useState(() => new Pubky());\n const [session, setSession] = useState(null);\n \n return (\n \n {children}\n \n );\n}\n\nexport const usePubky = () => useContext(PubkyContext);\n\`\`\`\n\n`; + break; + } + default: { + output += `## 1. Install Package\n\n\`\`\`bash\nnpm install @synonymdev/pubky\n\`\`\`\n\n`; + output += `## 2. Basic Integration\n\n\`\`\`${analysis.type}\n${templates['js-basic-app'].code}\n\`\`\`\n\n`; + break; + } + } + + return { + content: [{ type: 'text', text: output }], + }; + } + + private async integratePubky( + projectPath: string + ): Promise<{ content: Array<{ type: string; text: string }> }> { + // This would actually add files and modify project + // For now, we'll suggest what to do + return await this.adaptToProject(projectPath); + } + + // ============================================================================ + // 4. NEXUS API TOOLS + // ============================================================================ + // Social data querying, API exploration, client generation + + private async queryNexusApi( + query: string + ): Promise<{ content: Array<{ type: string; text: string }> }> { + const result = await this.nexusParser.searchEndpoints(query); + return createTextResponse(result); + } + + private async explainNexusEndpoint( + operationId: string + ): Promise<{ content: Array<{ type: string; text: string }> }> { + const example = await this.nexusParser.generateEndpointExample(operationId); + return createTextResponse(example); + } + + private async generateNexusClient( + language: string, + endpoints: string[] + ): Promise<{ content: Array<{ type: string; text: string }> }> { + let output = `# Nexus API Client\n\n`; + output += `Generated client for ${language}\n\n`; + + if (language === 'javascript' || language === 'typescript') { + output += `\`\`\`${language}\n`; + output += `const NEXUS_API_URL = 'https://nexus.example.com';\n\n`; + output += `class NexusClient {\n`; + output += ` constructor(apiUrl = NEXUS_API_URL) {\n`; + output += ` this.apiUrl = apiUrl;\n`; + output += ` }\n\n`; + + const commonEndpoints = endpoints.length > 0 ? endpoints : ['post_view_handler', 'stream_posts_handler', 'user_view_handler']; + + for (const operationId of commonEndpoints) { + try { + const example = await this.nexusParser.generateEndpointExample(operationId); + // Extract just the function from the example + const functionMatch = example.match(/async function[\s\S]*?}\n/); + if (functionMatch) { + output += ` ${functionMatch[0]}\n`; + } + } catch { + // Skip if endpoint not found + } + } + + output += `}\n\n`; + output += `export default NexusClient;\n`; + output += `\`\`\`\n`; + } else if (language === 'rust') { + output += `\`\`\`rust\n`; + output += `use reqwest;\nuse serde::{Deserialize, Serialize};\n\n`; + output += `pub struct NexusClient {\n`; + output += ` api_url: String,\n`; + output += ` client: reqwest::Client,\n`; + output += `}\n\n`; + output += `impl NexusClient {\n`; + output += ` pub fn new(api_url: impl Into) -> Self {\n`; + output += ` Self {\n`; + output += ` api_url: api_url.into(),\n`; + output += ` client: reqwest::Client::new(),\n`; + output += ` }\n`; + output += ` }\n\n`; + output += ` pub async fn get_post(&self, author_id: &str, post_id: &str) -> Result {\n`; + output += ` let url = format!("{}/v0/post/{}/{}", self.api_url, author_id, post_id);\n`; + output += ` self.client.get(&url).send().await?.json().await\n`; + output += ` }\n`; + output += `}\n`; + output += `\`\`\`\n`; + } + + return { + content: [{ type: 'text', text: output }], + }; + } + + // ============================================================================ + // 5. APP SPECS TOOLS + // ============================================================================ + // Data model generation, validation, examples + + private async generateDataModel( + model: string, + language: string + ): Promise<{ content: Array<{ type: string; text: string }> }> { + const example = await this.specsParser.generateModelExample(model, language as any); + return createTextResponse(example); + } + + private async validateModelData( + model: string, + data: any + ): Promise<{ content: Array<{ type: string; text: string }> }> { + const validation = await this.specsParser.validateModelData(model, data); + return createTextResponse(validation); + } + + private async explainModel( + model: string + ): Promise<{ content: Array<{ type: string; text: string }> }> { + const info = await this.specsParser.getModelInfo(model); + return createTextResponse(info); + } + + private async createModelExample( + model: string, + language: string + ): Promise<{ content: Array<{ type: string; text: string }> }> { + const example = await this.specsParser.generateModelExample(model, language as any); + return createTextResponse(example); + } + + // ============================================================================ + // 6. PKARR TOOLS (Discovery Layer) + // ============================================================================ + // Public-Key Addressable Resource Records: DNS, DHT, relay management + + private async getPkarrConcept( + concept: string + ): Promise<{ content: Array<{ type: string; text: string }> }> { + const conceptMap: Record = { + discovery: { + doc: 'base', + description: 'How Pkarr enables discovery of homeservers via public keys', + }, + dht: { + doc: 'base', + description: 'Mainline DHT - the 10M node distributed hash table powering Pkarr', + }, + relay: { + doc: 'relays', + description: 'HTTP relay servers for web apps and browsers', + }, + 'signed-packet': { + doc: 'base', + description: 'Cryptographically signed DNS packets published to DHT', + }, + 'dns-records': { + doc: 'base', + description: 'DNS resource records (A, AAAA, TXT, CNAME, NS, HTTPS, SVCB)', + }, + republishing: { + doc: 'base', + description: 'Keeping records alive by periodic republishing', + }, + keypair: { + doc: 'base', + description: 'Ed25519 keypairs for signing and verifying records', + }, + mainline: { + doc: 'base', + description: 'Mainline DHT (BEP44) - the backbone of Pkarr', + }, + }; + + const info = conceptMap[concept.toLowerCase()] || conceptMap['discovery']; + const designDoc = await this.fileReader.readPkarrDesignDoc(info.doc); + + let output = `# Pkarr Concept: ${concept}\n\n`; + output += `${info.description}\n\n`; + output += `---\n\n`; + output += designDoc; + + return { + content: [{ type: 'text', text: output }], + }; + } + + private async searchPkarrDocs( + query: string + ): Promise<{ content: Array<{ type: string; text: string }> }> { + const pkarrPaths = this.fileReader.getPkarrPaths(); + if (!pkarrPaths) { + return { + content: [{ type: 'text', text: 'Pkarr resources not available' }], + }; + } + + const results = await this.fileReader.searchFiles(pkarrPaths.root, query); + + let output = `# Search Results for "${query}"\n\n`; + output += `Found ${results.length} ${results.length === 1 ? 'file' : 'files'} matching your query:\n\n`; + + for (const result of results) { + const relativePath = result.path.replace(pkarrPaths.root, ''); + output += `## ${relativePath}\n\n`; + for (const match of result.matches) { + output += `- ${match}\n`; + } + output += `\n`; + } + + if (results.length === 0) { + output += `No matches found. Try searching for:\n`; + output += `- "publish" - Publishing records\n`; + output += `- "resolve" - Resolving public keys\n`; + output += `- "relay" - Relay configuration\n`; + output += `- "DHT" - Distributed Hash Table\n`; + } + + return { + content: [{ type: 'text', text: output }], + }; + } + + private async getPkarrExample( + language: string, + example: string + ): Promise<{ content: Array<{ type: string; text: string }> }> { + if (language === 'rust') { + const exampleContent = await this.fileReader.readPkarrExample(example); + const readmeContent = await this.fileReader.readPkarrFile('pkarr/examples/README.md'); + + let output = `# Pkarr ${example} Example (Rust)\n\n`; + output += readmeContent + '\n\n'; + output += `## Source Code\n\n\`\`\`rust\n${exampleContent}\n\`\`\`\n`; + + return { + content: [{ type: 'text', text: output }], + }; + } else { + // JavaScript + const pkgReadme = await this.fileReader.readPkarrJsBindings('pkg/README.md'); + const exampleJs = await this.fileReader.readPkarrJsBindings('pkg/example.js'); + + let output = `# Pkarr JavaScript Examples\n\n`; + output += pkgReadme + '\n\n'; + output += `## Example Code\n\n\`\`\`javascript\n${exampleJs}\n\`\`\`\n`; + + return { + content: [{ type: 'text', text: output }], + }; + } + } + + private async generatePkarrClient( + language: string, + includePublish?: boolean, + includeResolve?: boolean + ): Promise<{ content: Array<{ type: string; text: string }> }> { + const shouldPublish = includePublish !== false; + const shouldResolve = includeResolve !== false; + + let output = `# Pkarr Client - ${language}\n\n`; + + if (language === 'javascript' || language === 'typescript') { + const isTs = language === 'typescript'; + output += `\`\`\`${language}\n`; + output += `${isTs ? "import { Client, Keypair, SignedPacket } from 'pkarr';\n\n" : "const { Client, Keypair, SignedPacket } = require('pkarr');\n\n"}`; + output += `class PkarrClient {\n`; + output += ` ${isTs ? 'private client: Client;\n ' : ''}constructor(relays${isTs ? ': string[]' : ''} = ['${DEFAULT_PKARR_RELAYS[0]}', '${DEFAULT_PKARR_RELAYS[1]}']) {\n`; + output += ` this.client = new Client(relays);\n`; + output += ` }\n\n`; + + if (shouldPublish) { + output += ` async publish(keypair${isTs ? ': Keypair' : ''}, records${isTs ? ': { name: string; value: string; ttl: number }[]' : ''})${isTs ? ': Promise' : ''} {\n`; + output += ` const builder = SignedPacket.builder();\n`; + output += ` for (const record of records) {\n`; + output += ` builder.addTxtRecord(record.name, record.value, record.ttl);\n`; + output += ` }\n`; + output += ` const packet = builder.buildAndSign(keypair);\n`; + output += ` await this.client.publish(packet);\n`; + output += ` }\n\n`; + } + + if (shouldResolve) { + output += ` async resolve(publicKey${isTs ? ': string' : ''})${isTs ? ': Promise' : ''} {\n`; + output += ` return await this.client.resolve(publicKey);\n`; + output += ` }\n\n`; + } + + output += `}\n\n`; + output += `${isTs ? 'export default PkarrClient;\n' : 'module.exports = PkarrClient;\n'}`; + output += `\`\`\`\n`; + } else { + // Rust + output += `\`\`\`rust\n`; + output += `use pkarr::{Client, Keypair, SignedPacket};\nuse std::error::Error;\n\n`; + output += `pub struct PkarrClient {\n`; + output += ` client: Client,\n`; + output += `}\n\n`; + output += `impl PkarrClient {\n`; + output += ` pub fn new() -> Self {\n`; + output += ` Self {\n`; + output += ` client: Client::default(),\n`; + output += ` }\n`; + output += ` }\n\n`; + + if (shouldPublish) { + output += ` pub async fn publish(&self, keypair: &Keypair, packet: &SignedPacket) -> Result<(), Box> {\n`; + output += ` self.client.publish(packet).await?;\n`; + output += ` Ok(())\n`; + output += ` }\n\n`; + } + + if (shouldResolve) { + output += ` pub async fn resolve(&self, public_key: &str) -> Result, Box> {\n`; + output += ` let packet = self.client.resolve(public_key).await?;\n`; + output += ` Ok(packet)\n`; + output += ` }\n\n`; + } + + output += `}\n`; + output += `\`\`\`\n`; + } + + return { + content: [{ type: 'text', text: output }], + }; + } + + private async startPkarrRelay( + port?: number, + testnet?: boolean + ): Promise<{ content: Array<{ type: string; text: string }> }> { + if (!this.pkarrRelayManager) { + return { + content: [ + { + type: 'text', + text: 'Pkarr relay manager not available. Make sure Pkarr resources are installed.', + }, + ], + }; + } + + try { + const config = { port, testnet }; + const result = await this.pkarrRelayManager.start(config); + return { + content: [{ type: 'text', text: result }], + }; + } catch (error: any) { + return { + content: [ + { + type: 'text', + text: `Failed to start Pkarr relay: ${error.message}\n\nMake sure Rust and Cargo are installed.`, + }, + ], + }; + } + } + + private async stopPkarrRelay(): Promise<{ content: Array<{ type: string; text: string }> }> { + if (!this.pkarrRelayManager) { + return createTextResponse('Pkarr relay manager not available'); + } + + const result = await this.pkarrRelayManager.stop(); + return createTextResponse(result); + } + + private async restartPkarrRelay( + port?: number, + testnet?: boolean + ): Promise<{ content: Array<{ type: string; text: string }> }> { + if (!this.pkarrRelayManager) { + return { + content: [{ type: 'text', text: 'Pkarr relay manager not available' }], + }; + } + + const config = { port, testnet }; + const result = await this.pkarrRelayManager.restart(config); + return { + content: [{ type: 'text', text: result }], + }; + } + + private async checkPkarrRelayStatus(): Promise<{ + content: Array<{ type: string; text: string }>; + }> { + if (!this.pkarrRelayManager) { + return { + content: [{ type: 'text', text: 'Pkarr relay manager not available' }], + }; + } + + const isRunning = await this.pkarrRelayManager.isRunning(); + const status = isRunning ? 'βœ… Pkarr relay is running' : '❌ Pkarr relay is not running'; + + if (isRunning) { + const info = await this.pkarrRelayManager.getInfo(); + return { + content: [ + { + type: 'text', + text: `${status}\n\nURL: ${info.url}\nPort: ${info.port}${info.testnet ? '\nMode: Testnet' : ''}`, + }, + ], + }; + } + + return { + content: [ + { + type: 'text', + text: `${status}\n\nStart it with the \`start_pkarr_relay\` tool.`, + }, + ], + }; + } + + private async getPkarrRelayInfo(): Promise<{ content: Array<{ type: string; text: string }> }> { + if (!this.pkarrRelayManager) { + return { + content: [{ type: 'text', text: 'Pkarr relay manager not available' }], + }; + } + + const info = await this.pkarrRelayManager.getInfo(); + + let output = `# Pkarr Relay Information\n\n`; + output += `**Status**: ${info.running ? 'βœ… Running' : '❌ Not running'}\n\n`; + + if (info.running) { + output += `**URL**: ${info.url}\n`; + output += `**Port**: ${info.port}\n`; + if (info.cacheLocation) { + output += `**Cache Location**: ${info.cacheLocation}\n`; + } + if (info.testnet) { + output += `**Mode**: Testnet\n`; + } + output += `\n## Usage in Code\n\n`; + output += `### JavaScript\n\`\`\`javascript\nconst client = new Client(['${info.url}']);\n\`\`\`\n\n`; + output += `### Rust\n\`\`\`rust\nlet client = Client::builder().relay("${info.url}").build();\n\`\`\`\n`; + } else { + output += `Start the relay with \`start_pkarr_relay\` tool.\n`; + } + + return { + content: [{ type: 'text', text: output }], + }; + } + + private async generatePkarrKeypair( + language: string + ): Promise<{ content: Array<{ type: string; text: string }> }> { + let output = `# Generate Pkarr Keypair - ${language}\n\n`; + + if (language === 'javascript' || language === 'typescript') { + const isTs = language === 'typescript'; + output += `\`\`\`${language}\n`; + output += `${isTs ? "import { Keypair } from 'pkarr';\n\n" : "const { Keypair } = require('pkarr');\n\n"}`; + output += `// Generate a new random keypair\n`; + output += `const keypair = new Keypair();\n\n`; + output += `// Get the public key (z-base32 encoded)\n`; + output += `const publicKey = keypair.public_key_string();\n`; + output += `console.log('Public Key:', publicKey);\n\n`; + output += `// Get secret key bytes (save securely!)\n`; + output += `const secretBytes = keypair.secret_key_bytes();\n\n`; + output += `// Recreate keypair from secret key later\n`; + output += `// const restoredKeypair = Keypair.from_secret_key(secretBytes);\n`; + output += `\`\`\`\n`; + } else { + // Rust + output += `\`\`\`rust\n`; + output += `use pkarr::Keypair;\n\n`; + output += `// Generate a new random keypair\n`; + output += `let keypair = Keypair::random();\n\n`; + output += `// Get the public key\n`; + output += `let public_key = keypair.public_key();\n`; + output += `println!("Public Key: {}", public_key);\n\n`; + output += `// Get secret key bytes (save securely!)\n`; + output += `let secret_bytes = keypair.secret_key().to_bytes();\n\n`; + output += `// Recreate keypair from secret key later\n`; + output += `// let restored_keypair = Keypair::from_secret_key(&secret_bytes)?;\n`; + output += `\`\`\`\n`; + } + + output += `\n⚠️ **Important**: Store the secret key securely! Anyone with access to it can update your DNS records.\n`; + + return { + content: [{ type: 'text', text: output }], + }; + } + + private async generateDnsRecordBuilder( + language: string, + recordTypes?: string[] + ): Promise<{ content: Array<{ type: string; text: string }> }> { + const types = recordTypes || ['A', 'TXT', 'HTTPS']; + + let output = `# DNS Record Builder - ${language}\n\n`; + output += `Building ${types.join(', ')} records\n\n`; + + if (language === 'javascript' || language === 'typescript') { + const isTs = language === 'typescript'; + output += `\`\`\`${language}\n`; + output += `${isTs ? "import { SignedPacket, Keypair } from 'pkarr';\n\n" : "const { SignedPacket, Keypair } = require('pkarr');\n\n"}`; + output += `const builder = SignedPacket.builder();\n\n`; + + for (const type of types) { + switch (type.toUpperCase()) { + case 'A': + output += `// Add A record (IPv4)\n`; + output += `builder.addARecord("www", "192.168.1.1", 3600);\n\n`; + break; + case 'AAAA': + output += `// Add AAAA record (IPv6)\n`; + output += `builder.addAAAARecord("www", "2001:db8::1", 3600);\n\n`; + break; + case 'TXT': + output += `// Add TXT record\n`; + output += `builder.addTxtRecord("_service", "pkarr=v1.0", 3600);\n\n`; + break; + case 'CNAME': + output += `// Add CNAME record\n`; + output += `builder.addCnameRecord("alias", "www", 3600);\n\n`; + break; + case 'HTTPS': + output += `// Add HTTPS record with service parameters\n`; + output += `builder.addHttpsRecord("@", 1, ".", 3600, {\n`; + output += ` port: 443,\n`; + output += ` ipv4hint: "192.168.1.1",\n`; + output += ` alpn: ["h2", "http/1.1"]\n`; + output += `});\n\n`; + break; + } + } + + output += `// Build and sign\n`; + output += `const keypair = new Keypair();\n`; + output += `const packet = builder.buildAndSign(keypair);\n`; + output += `\`\`\`\n`; + } else { + // Rust + output += `\`\`\`rust\n`; + output += `use pkarr::{SignedPacket, Keypair};\n`; + output += `use simple_dns::{Name, CLASS, ResourceRecord, rdata::*};\n\n`; + output += `let mut packet = SignedPacket::new(&keypair)?;\n\n`; + + for (const type of types) { + switch (type.toUpperCase()) { + case 'A': + output += `// Add A record (IPv4)\n`; + output += `packet.add_answer(ResourceRecord::new(\n`; + output += ` Name::new_unchecked("www"),\n`; + output += ` CLASS::IN,\n`; + output += ` 3600,\n`; + output += ` A { address: [192, 168, 1, 1] }\n`; + output += `));\n\n`; + break; + case 'TXT': + output += `// Add TXT record\n`; + output += `packet.add_answer(ResourceRecord::new(\n`; + output += ` Name::new_unchecked("_service"),\n`; + output += ` CLASS::IN,\n`; + output += ` 3600,\n`; + output += ` TXT::new().with_string("pkarr=v1.0")\n`; + output += `));\n\n`; + break; + } + } + + output += `\`\`\`\n`; + } + + return { + content: [{ type: 'text', text: output }], + }; + } + + private async explainPkarrKey( + publicKey: string + ): Promise<{ content: Array<{ type: string; text: string }> }> { + let output = `# Pkarr Public Key Analysis\n\n`; + output += `**Key**: \`${publicKey}\`\n\n`; + + // Basic validation + if (publicKey.length !== 52) { + output += `⚠️ **Warning**: Standard Pkarr keys are 52 characters (z-base32 encoded ed25519 public keys)\n\n`; + } + + output += `## Key Properties\n\n`; + output += `- **Encoding**: z-base32 (base32 with alternative alphabet)\n`; + output += `- **Key Type**: Ed25519 public key (32 bytes)\n`; + output += `- **Length**: ${publicKey.length} characters\n\n`; + + output += `## Usage\n\n`; + output += `This key can be used as:\n\n`; + output += `1. **Top-Level Domain**: \`https://${publicKey}\`\n`; + output += `2. **DHT Key**: Lookup signed packets from Mainline DHT\n`; + output += `3. **Verification**: Verify signatures on DNS packets\n\n`; + + output += `## Resolve This Key\n\n`; + output += `### JavaScript\n\`\`\`javascript\n`; + output += `const client = new Client();\n`; + output += `const packet = await client.resolve("${publicKey}");\n`; + output += `if (packet) {\n`; + output += ` console.log('Records:', packet.records);\n`; + output += `}\n`; + output += `\`\`\`\n\n`; + + output += `### Rust\n\`\`\`rust\n`; + output += `let client = Client::default();\n`; + output += `if let Some(packet) = client.resolve("${publicKey}").await? {\n`; + output += ` println!("Records: {:?}", packet);\n`; + output += `}\n`; + output += `\`\`\`\n`; + + return { + content: [{ type: 'text', text: output }], + }; + } + + private async installPkarrRelay(): Promise<{ content: Array<{ type: string; text: string }> }> { + try { + const { exec } = await import('child_process'); + const { promisify } = await import('util'); + const execAsync = promisify(exec); + + const { stdout, stderr } = await execAsync('cargo install pkarr-relay', { + timeout: 300000, // 5 minutes + }); + + let output = `βœ… Pkarr relay installed successfully!\n\n`; + output += `Installation output:\n${stdout}\n${stderr}\n\n`; + output += `You can now start the relay with the \`start_pkarr_relay\` tool.`; + + return { + content: [{ type: 'text', text: output }], + }; + } catch (error: any) { + return { + content: [ + { + type: 'text', + text: `Failed to install pkarr-relay: ${error.message}\n\nMake sure Rust and Cargo are installed:\nhttps://rustup.rs/`, + }, + ], + }; + } + } + + private async setupPkarrProject( + projectPath: string + ): Promise<{ content: Array<{ type: string; text: string }> }> { + const analysis = await this.envDetector.analyzeProject(projectPath); + + let output = `# Add Pkarr to Your Project\n\n`; + + switch (analysis.type) { + case PROJECT_TYPES.RUST: { + output += `## Add Dependency\n\nAdd to your Cargo.toml:\n\`\`\`toml\n[dependencies]\npkarr = "5.0"\n\`\`\`\n\n`; + output += `## Basic Usage\n\n\`\`\`rust\n`; + output += `use pkarr::{Client, Keypair, SignedPacket};\n\n`; + output += `#[tokio::main]\nasync fn main() -> Result<(), Box> {\n`; + output += ` let keypair = Keypair::random();\n`; + output += ` let client = Client::default();\n`; + output += ` \n`; + output += ` // Publish records\n`; + output += ` let packet = SignedPacket::new(&keypair)?;\n`; + output += ` client.publish(&packet).await?;\n`; + output += ` \n`; + output += ` Ok(())\n`; + output += `}\n\`\`\`\n`; + break; + } + case PROJECT_TYPES.JAVASCRIPT: + case PROJECT_TYPES.TYPESCRIPT: { + output += `## Install Package\n\n\`\`\`bash\nnpm install pkarr\n\`\`\`\n\n`; + output += `## Basic Usage\n\n\`\`\`${analysis.type}\n`; + if (analysis.type === 'typescript') { + output += `import { Client, Keypair, SignedPacket } from 'pkarr';\n\n`; + } else { + output += `const { Client, Keypair, SignedPacket } = require('pkarr');\n\n`; + } + output += `const keypair = new Keypair();\n`; + output += `const client = new Client();\n\n`; + output += `// Publish records\n`; + output += `const builder = SignedPacket.builder();\n`; + output += `builder.addTxtRecord("_service", "myapp=v1", 3600);\n`; + output += `const packet = builder.buildAndSign(keypair);\n`; + output += `await client.publish(packet);\n\n`; + output += `// Resolve records\n`; + output += `const resolved = await client.resolve(keypair.public_key_string());\n`; + output += `console.log(resolved.records);\n`; + output += `\`\`\`\n`; + break; + } + default: + output += `Project type not detected. Install Pkarr manually:\n\n`; + output += `**JavaScript/TypeScript**: \`npm install pkarr\`\n`; + output += `**Rust**: Add \`pkarr = "5.0"\` to Cargo.toml\n`; + } + + return { + content: [{ type: 'text', text: output }], + }; + } + + // ============================================================================ + // 7. PKDNS TOOLS (DNS Resolver for Pkarr Domains) + // ============================================================================ + // Browser/system DNS setup, server installation + + private async getPkdnsInfo(): Promise<{ content: Array<{ type: string; text: string }> }> { + const readme = await this.fileReader.readPkdnsFile('README.md'); + const servers = await this.fileReader.readPkdnsFile('servers.txt'); + + let output = `# Pkdns - DNS Resolver for Pkarr Domains\n\n`; + output += readme + '\n\n'; + output += `## Public Pkdns Servers\n\n\`\`\`\n${servers}\n\`\`\`\n`; + + return createTextResponse(output); + } + + private async setupPkdnsBrowser(): Promise<{ content: Array<{ type: string; text: string }> }> { + const dohDoc = await this.fileReader.readPkdnsDoc('dns-over-https'); + + let output = `# Configure Your Browser for Pkdns\n\n`; + output += dohDoc + '\n\n'; + output += `## Quick Setup\n\n`; + output += `1. Pick a DNS-over-HTTPS server from public list\n`; + output += `2. Open browser settings\n`; + output += `3. Find "DNS over HTTPS" or "Secure DNS"\n`; + output += `4. Enter the DoH URL\n\n`; + output += `## Test It\n\n`; + output += `Visit: http://7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy/\n`; + + return createTextResponse(output); + } + + private async setupPkdnsSystem(): Promise<{ content: Array<{ type: string; text: string }> }> { + let output = `# Configure System DNS for Pkdns\n\n`; + output += `## Why?\n\n`; + output += `Makes Pkarr domains work system-wide in all applications.\n\n`; + output += `## Steps\n\n`; + output += `1. Find a public pkdns server IP (use \`get_pkdns_info\` tool)\n`; + output += `2. Add it to your system DNS settings\n\n`; + output += `### macOS\n`; + output += `System Preferences β†’ Network β†’ Advanced β†’ DNS β†’ Add server IP\n\n`; + output += `### Linux (Ubuntu)\n`; + output += `\`\`\`bash\n`; + output += `sudo nano /etc/resolv.conf\n`; + output += `# Add: nameserver YOUR_PKDNS_IP\n`; + output += `\`\`\`\n\n`; + output += `### Windows\n`; + output += `Control Panel β†’ Network β†’ Change adapter settings β†’ Properties β†’ IPv4 β†’ DNS\n\n`; + output += `## Test It\n\n`; + output += `\`\`\`bash\n`; + output += `nslookup 7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy\n`; + output += `\`\`\`\n`; + + return createTextResponse(output); + } + + private async installPkdns(): Promise<{ content: Array<{ type: string; text: string }> }> { + try { + const { exec } = await import('child_process'); + const { promisify } = await import('util'); + const execAsync = promisify(exec); + + const { stdout, stderr } = await execAsync('cargo install --git https://github.com/pubky/pkdns pkdns', { + timeout: 300000, // 5 minutes + }); + + return createTextResponse( + `βœ… Pkdns installed successfully!\n\n${stdout}\n${stderr}\n\nRun with: \`pkdns --verbose\`\n\nThen configure your browser or system DNS.` + ); + } catch (error: any) { + return createTextResponse( + `Failed to install pkdns: ${error.message}\n\nMake sure Rust and Cargo are installed:\nhttps://rustup.rs/` + ); + } + } + + // ============================================================================ + // 8. NEXUS IMPLEMENTATION TOOLS (For Advanced Users) + // ============================================================================ + // Architecture understanding, development setup, component details + + private async getNexusArchitecture(): Promise<{ content: Array<{ type: string; text: string }> }> { + const readme = await this.fileReader.readNexusFile('README.md'); + const docsReadme = await this.fileReader.readNexusDoc('readme.md'); + + let output = `# Pubky Nexus Architecture\n\n`; + output += `## Overview\n\n${readme}\n\n`; + output += `## Detailed Architecture\n\n${docsReadme}\n\n`; + output += `## Components\n\n`; + output += `- **nexus-watcher**: Event aggregator (listens to homeservers)\n`; + output += `- **nexus-service**: REST API server\n`; + output += `- **nexus-common**: Shared database and models\n`; + output += `- **nexusd**: Daemon manager\n\n`; + output += `Use \`explain_nexus_component\` tool to learn more about each component.`; + + return createTextResponse(output); + } + + private async setupNexusDev(): Promise<{ content: Array<{ type: string; text: string }> }> { + let output = `# Set Up Nexus Development Environment\n\n`; + output += `Nexus requires Neo4j (graph database) and Redis (cache).\n\n`; + output += `## Quick Start\n\n`; + output += `\`\`\`bash\n`; + output += `cd /path/to/pubky-nexus\n`; + output += `cd docker\n`; + output += `cp .env-sample .env\n`; + output += `docker compose up -d\n`; + output += `\`\`\`\n\n`; + output += `## Run Nexus\n\n`; + output += `\`\`\`bash\n`; + output += `cargo run -p nexusd\n`; + output += `# Or run components separately:\n`; + output += `cargo run -p nexusd -- watcher\n`; + output += `cargo run -p nexusd -- api\n`; + output += `\`\`\`\n\n`; + output += `## Access UIs\n\n`; + output += `- Swagger API: http://localhost:8080/swagger-ui\n`; + output += `- Redis Insight: http://localhost:8001/redis-stack/browser\n`; + output += `- Neo4j Browser: http://localhost:7474/browser/\n\n`; + output += `See full README for testing, benchmarking, and migrations.`; + + return createTextResponse(output); + } + + private async explainNexusComponent( + component: string + ): Promise<{ content: Array<{ type: string; text: string }> }> { + const componentMap: Record = { + common: 'common', + watcher: 'watcher', + service: 'webapi', + webapi: 'webapi', + nexusd: 'common', // Fallback + }; + + const comp = componentMap[component] || 'common'; + const content = await this.fileReader.readNexusComponentReadme(comp); + + return createTextResponse(`# Nexus Component: ${component}\n\n${content}`); + } +} diff --git a/pubky-mcp-server/src/types.ts b/pubky-mcp-server/src/types.ts new file mode 100644 index 0000000..7da460c --- /dev/null +++ b/pubky-mcp-server/src/types.ts @@ -0,0 +1,124 @@ +/** + * Common types for the Pubky MCP Server + */ + +// Common response types +export type ToolResponse = { content: Array<{ type: string; text: string }> }; +export type PromptResponse = { messages: Array<{ role: string; content: { type: string; text: string } }> }; +export type ResourceResponse = { contents: { uri: string; mimeType: string; text: string }[] }; + +export interface PubkyCorePaths { + root: string; + docs: string; + examples: string; + examplesRust: string; + examplesJs: string; +} + +export interface PkarrPaths { + root: string; + design: string; + examples: string; + bindingsJs: string; + relay: string; +} + +export interface PkdnsPaths { + root: string; + docs: string; + cli: string; + serverConfig: string; +} + +export interface NexusPaths { + root: string; + docs: string; + examples: string; + componentReadmes: { + common: string; + watcher: string; + webapi: string; + }; +} + +export interface TestnetInfo { + isRunning: boolean; + homeserverPublicKey: string; + ports: { + dht: number; + pkarrRelay: number; + httpRelay: number; + homeserver: number; + adminServer: number; + }; + urls: { + homeserver: string; + admin: string; + httpRelay: string; + }; +} + +export interface ProjectAnalysis { + type: 'rust' | 'javascript' | 'typescript' | 'unknown'; + hasPackageJson: boolean; + hasCargoToml: boolean; + hasPubkyDependency: boolean; + framework?: string; + dependencies: string[]; + devDependencies: string[]; +} + +export interface EnvironmentInfo { + node?: { + version: string; + path: string; + }; + npm?: { + version: string; + path: string; + }; + cargo?: { + version: string; + path: string; + }; + rust?: { + version: string; + path: string; + }; + pubkyTestnet?: { + installed: boolean; + path?: string; + }; + os: string; + arch: string; +} + +export interface CodeTemplate { + name: string; + description: string; + language: 'rust' | 'javascript' | 'typescript'; + code: string; + dependencies?: string[]; +} + +export interface PkarrRelayInfo { + running: boolean; + port?: number; + cacheLocation?: string; + url?: string; + testnet?: boolean; +} + +export interface PkarrRelayConfig { + port?: number; + cachePath?: string; + cacheSize?: number; + minimumTtl?: number; + maximumTtl?: number; + testnet?: boolean; + rateLimiter?: { + behindProxy: boolean; + burstSize: number; + perSecond: number; + }; +} diff --git a/pubky-mcp-server/src/utils/app-specs.ts b/pubky-mcp-server/src/utils/app-specs.ts new file mode 100644 index 0000000..35c2e25 --- /dev/null +++ b/pubky-mcp-server/src/utils/app-specs.ts @@ -0,0 +1,439 @@ +/** + * App Specs Parser - Utilities for parsing and exposing pubky-app-specs model information + */ + +import * as fs from 'fs/promises'; +import * as path from 'path'; + +export interface ModelInfo { + name: string; + description: string; + uri: string; + fields: ModelField[]; + validation: ValidationRule[]; +} + +export interface ModelField { + name: string; + type: string; + description: string; + required: boolean; + validation?: string; +} + +export interface ValidationRule { + field: string; + rule: string; + description: string; +} + +export class AppSpecsParser { + private workspaceRoot: string; + private specsRoot: string; + private readmeContent: string | null = null; + + constructor(workspaceRoot: string) { + this.workspaceRoot = workspaceRoot; + this.specsRoot = path.join(workspaceRoot, 'pubky-app-specs'); + } + + async loadReadme(): Promise { + if (this.readmeContent) return; + + const readmePath = path.join(this.specsRoot, 'README.md'); + this.readmeContent = await fs.readFile(readmePath, 'utf-8'); + } + + async getOverview(): Promise { + await this.loadReadme(); + return this.readmeContent || 'README not found'; + } + + async getModelInfo(modelName: string): Promise { + await this.loadReadme(); + + const modelMap: Record = { + user: 'PubkyAppUser', + post: 'PubkyAppPost', + tag: 'PubkyAppTag', + bookmark: 'PubkyAppBookmark', + follow: 'PubkyAppFollow', + file: 'PubkyAppFile', + feed: 'PubkyAppFeed', + mute: 'PubkyAppMute', + 'last_read': 'PubkyAppLastRead', + blob: 'PubkyAppBlob', + }; + + const fullModelName = modelMap[modelName.toLowerCase()] || modelName; + + // Extract model section from README + const regex = new RegExp( + `### ${fullModelName}[\\s\\S]*?(?=###|## |$)`, + 'i' + ); + const match = this.readmeContent?.match(regex); + + if (!match) { + return `Model ${fullModelName} not found in documentation`; + } + + let output = `# ${fullModelName} Model\n\n`; + output += match[0]; + + // Add code example from model file + try { + const modelFileName = modelName.toLowerCase() + '.rs'; + const modelPath = path.join(this.specsRoot, 'src', 'models', modelFileName); + const modelCode = await fs.readFile(modelPath, 'utf-8'); + + // Extract struct definition + const structMatch = modelCode.match(/pub struct \w+[^}]+}/s); + if (structMatch) { + output += `\n\n## Rust Definition\n\n\`\`\`rust\n${structMatch[0]}\n\`\`\`\n`; + } + } catch { + // Model file not found, skip code + } + + return output; + } + + async generateModelExample(modelName: string, language: 'javascript' | 'typescript' | 'rust'): Promise { + const examples: Record = { + user: { + javascript: `import { PubkyAppUser, PubkySpecsBuilder } from 'pubky-app-specs'; + +// Create a user profile +const userId = '8kkppkmiubfq4pxn6f73nqrhhhgkb5xyfprntc9si3np9ydbotto'; +const specsBuilder = new PubkySpecsBuilder(userId); + +const { user, meta } = specsBuilder.createUser( + 'Alice Smith', // name + 'Software Developer', // bio + 'https://example.com/avatar.png', // image + [ + { title: 'GitHub', url: 'https://github.com/alice' }, + { title: 'Website', url: 'https://alice.dev' } + ], // links + 'Building on Pubky' // status +); + +console.log('User Profile URL:', meta.url); +console.log('User Data:', user.toJson());`, + rust: `use pubky_app_specs::{PubkyAppUser, PubkyAppUserLink, Validatable}; + +// Create a user profile +let user = PubkyAppUser::new( + "Alice Smith".to_string(), + Some("Software Developer".to_string()), + Some("https://example.com/avatar.png".to_string()), + Some(vec![ + PubkyAppUserLink::new( + "GitHub".to_string(), + "https://github.com/alice".to_string() + ), + PubkyAppUserLink::new( + "Website".to_string(), + "https://alice.dev".to_string() + ) + ]), + Some("Building on Pubky".to_string()) +); + +// Validate +user.validate(None)?; + +// Serialize to JSON +let json = serde_json::to_string(&user)?; +println!("User: {}", json);`, + }, + post: { + javascript: `import { PubkyAppPost, PubkyAppPostKind, PubkySpecsBuilder } from 'pubky-app-specs'; + +const userId = '8kkppkmiubfq4pxn6f73nqrhhhgkb5xyfprntc9si3np9ydbotto'; +const specsBuilder = new PubkySpecsBuilder(userId); + +// Create a simple post +const { post, meta } = specsBuilder.createPost( + 'Hello, Pubky world! This is my first post.', + PubkyAppPostKind.Short, + null, // parent (for replies) + null, // embed (for reposts) + null // attachments +); + +console.log('Post ID:', meta.id); +console.log('Post URL:', meta.url); +console.log('Post Data:', post.toJson()); + +// Create a reply +const { post: reply } = specsBuilder.createPost( + 'This is a reply!', + PubkyAppPostKind.Short, + meta.url, // parent post + null, + null +);`, + rust: `use pubky_app_specs::{PubkyAppPost, PubkyAppPostKind, TimestampId, Validatable}; + +// Create a post +let post = PubkyAppPost::new( + "Hello, Pubky world! This is my first post.".to_string(), + PubkyAppPostKind::Short, + None, // parent + None, // embed + None // attachments +); + +// Generate ID +let post_id = post.create_id(); + +// Validate +post.validate(Some(&post_id))?; + +// Create path +let path = PubkyAppPost::create_path(&post_id); +println!("Post path: {}", path); + +// Serialize +let json = serde_json::to_string(&post)?;`, + }, + tag: { + javascript: `import { PubkySpecsBuilder } from 'pubky-app-specs'; + +const userId = '8kkppkmiubfq4pxn6f73nqrhhhgkb5xyfprntc9si3np9ydbotto'; +const specsBuilder = new PubkySpecsBuilder(userId); + +// Tag a post +const postUri = 'pubky://user_id/pub/pubky.app/posts/0000000000000'; +const { tag, meta } = specsBuilder.createTag(postUri, 'bitcoin'); + +console.log('Tag ID:', meta.id); +console.log('Tag URL:', meta.url); +console.log('Tag Data:', tag.toJson());`, + rust: `use pubky_app_specs::{PubkyAppTag, HashId, Validatable}; + +let tag = PubkyAppTag::new( + "pubky://user_id/pub/pubky.app/posts/0000000000000".to_string(), + "bitcoin".to_string() +); + +let tag_id = tag.create_id(); +tag.validate(Some(&tag_id))?; + +let json = serde_json::to_string(&tag)?;`, + }, + bookmark: { + javascript: `import { PubkySpecsBuilder } from 'pubky-app-specs'; + +const userId = '8kkppkmiubfq4pxn6f73nqrhhhgkb5xyfprntc9si3np9ydbotto'; +const specsBuilder = new PubkySpecsBuilder(userId); + +const postUri = 'pubky://user_id/pub/pubky.app/posts/0000000000000'; +const { bookmark, meta } = specsBuilder.createBookmark(postUri); + +console.log('Bookmark ID:', meta.id); +console.log('Bookmark URL:', meta.url);`, + rust: `use pubky_app_specs::{PubkyAppBookmark, HashId}; + +let bookmark = PubkyAppBookmark::new( + "pubky://user_id/pub/pubky.app/posts/0000000000000".to_string() +); + +let bookmark_id = bookmark.create_id(); +let path = PubkyAppBookmark::create_path(&bookmark_id);`, + }, + follow: { + javascript: `import { PubkySpecsBuilder } from 'pubky-app-specs'; + +const userId = '8kkppkmiubfq4pxn6f73nqrhhhgkb5xyfprntc9si3np9ydbotto'; +const specsBuilder = new PubkySpecsBuilder(userId); + +const followUserId = 'dzswkfy7ek3bqnoc89jxuqqfbzhjrj6mi8qthgbxxcqkdugm3rio'; +const { follow, meta } = specsBuilder.createFollow(followUserId); + +console.log('Follow URL:', meta.url);`, + rust: `use pubky_app_specs::PubkyAppFollow; + +let follow = PubkyAppFollow::new(); +let user_id = "user_to_follow_id"; +let path = PubkyAppFollow::create_path(user_id);`, + }, + file: { + javascript: `import { PubkySpecsBuilder } from 'pubky-app-specs'; + +const userId = '8kkppkmiubfq4pxn6f73nqrhhhgkb5xyfprntc9si3np9ydbotto'; +const specsBuilder = new PubkySpecsBuilder(userId); + +// First create a blob (actual file data) +const fileData = new Uint8Array([/* file bytes */]); +const { blob, meta: blobMeta } = specsBuilder.createBlob(Array.from(fileData)); + +// Then create file metadata +const { file, meta } = specsBuilder.createFile( + 'my-document.pdf', + blobMeta.url, + 'application/pdf', + fileData.length +); + +console.log('File ID:', meta.id); +console.log('File URL:', meta.url);`, + rust: `use pubky_app_specs::{PubkyAppFile, TimestampId}; + +let file = PubkyAppFile::new( + "my-document.pdf".to_string(), + "pubky://user_id/pub/pubky.app/blobs/0000000000000".to_string(), + "application/pdf".to_string(), + 1024 * 500 // 500KB +); + +let file_id = file.create_id(); +let path = PubkyAppFile::create_path(&file_id);`, + }, + feed: { + javascript: `import { PubkySpecsBuilder } from 'pubky-app-specs'; + +const userId = '8kkppkmiubfq4pxn6f73nqrhhhgkb5xyfprntc9si3np9ydbotto'; +const specsBuilder = new PubkySpecsBuilder(userId); + +const { feed, meta } = specsBuilder.createFeed( + ['bitcoin', 'nostr'], // tags + 'friends', // reach + 'columns', // layout + 'recent', // sort + 'short', // content type + 'My Bitcoin Feed' // name +); + +console.log('Feed ID:', meta.id);`, + rust: `use pubky_app_specs::{PubkyAppFeed, PubkyAppFeedReach, PubkyAppFeedLayout, PubkyAppFeedSort}; + +let feed = PubkyAppFeed::new( + Some(vec!["bitcoin".to_string(), "nostr".to_string()]), + PubkyAppFeedReach::Friends, + PubkyAppFeedLayout::Columns, + PubkyAppFeedSort::Recent, + Some("short".to_string()), + "My Bitcoin Feed".to_string() +);`, + }, + }; + + const modelExamples = examples[modelName.toLowerCase()]; + if (!modelExamples) { + return `No example available for model: ${modelName}`; + } + + const code = modelExamples[language] || modelExamples.javascript; + + let output = `# ${modelName.charAt(0).toUpperCase() + modelName.slice(1)} Example\n\n`; + output += `## ${language.charAt(0).toUpperCase() + language.slice(1)}\n\n`; + output += `\`\`\`${language === 'rust' ? 'rust' : 'javascript'}\n${code}\n\`\`\`\n`; + + return output; + } + + async getJavaScriptExamples(): Promise { + try { + const examplePath = path.join(this.specsRoot, 'pkg', 'example.js'); + const exampleContent = await fs.readFile(examplePath, 'utf-8'); + + let output = `# Pubky App Specs JavaScript Examples\n\n`; + output += `Complete working example showing how to use all data models:\n\n`; + output += `\`\`\`javascript\n${exampleContent}\n\`\`\`\n`; + + return output; + } catch { + return 'JavaScript examples not found'; + } + } + + async validateModelData(modelName: string, data: any): Promise { + // This would require actually loading and running the validation + // For now, provide validation rules + const validationRules: Record = { + user: [ + 'name: 3-50 characters, cannot be "[DELETED]"', + 'bio: max 160 characters (optional)', + 'image: valid URL, max 300 characters (optional)', + 'links: max 5 links, each with title (max 100 chars) and valid URL (max 300 chars)', + 'status: max 50 characters (optional)', + ], + post: [ + 'content: max 2000 chars (short) or 50000 chars (long), cannot be "[DELETED]"', + 'kind: must be one of: short, long, image, video, link, file', + 'parent: must be valid URI if present', + 'embed: URI must be valid if present', + 'attachments: each must be valid URI', + ], + tag: [ + 'uri: must be valid URI', + 'label: trimmed, lowercase, max 20 characters', + 'created_at: required timestamp', + ], + bookmark: [ + 'uri: must be valid URI', + 'created_at: required timestamp', + ], + follow: ['created_at: required timestamp'], + file: [ + 'name: 1-255 characters', + 'src: must be valid URL, max 1024 characters', + 'content_type: must be valid IANA MIME type', + 'size: positive integer, max 10MB', + 'created_at: required timestamp', + ], + }; + + const rules = validationRules[modelName.toLowerCase()] || []; + + let output = `# ${modelName} Validation\n\n`; + output += `## Validation Rules\n\n`; + + for (const rule of rules) { + output += `- ${rule}\n`; + } + + output += `\n## Provided Data\n\n`; + output += `\`\`\`json\n${JSON.stringify(data, null, 2)}\n\`\`\`\n`; + + return output; + } + + async searchModels(query: string): Promise { + await this.loadReadme(); + + if (!this.readmeContent) { + return 'README not loaded'; + } + + const queryLower = query.toLowerCase(); + const lines = this.readmeContent.split('\n'); + const matches: string[] = []; + + for (let i = 0; i < lines.length; i++) { + if (lines[i].toLowerCase().includes(queryLower)) { + // Include context: 2 lines before and after + const start = Math.max(0, i - 2); + const end = Math.min(lines.length, i + 3); + matches.push(lines.slice(start, end).join('\n')); + } + } + + if (matches.length === 0) { + return `No matches found for: ${query}`; + } + + let output = `# Search Results for "${query}"\n\n`; + output += `Found ${matches.length} match(es):\n\n`; + + for (let i = 0; i < Math.min(matches.length, 10); i++) { + output += `## Match ${i + 1}\n\n${matches[i]}\n\n---\n\n`; + } + + return output; + } +} + diff --git a/pubky-mcp-server/src/utils/environment.ts b/pubky-mcp-server/src/utils/environment.ts new file mode 100644 index 0000000..f8cf152 --- /dev/null +++ b/pubky-mcp-server/src/utils/environment.ts @@ -0,0 +1,217 @@ +/** + * Utilities for detecting and managing development environment + */ + +import { exec } from 'child_process'; +import { promisify } from 'util'; +import * as fs from 'fs/promises'; +import * as path from 'path'; +import * as os from 'os'; +import type { EnvironmentInfo, ProjectAnalysis } from '../types.js'; +import { PROJECT_TYPES, FRAMEWORKS } from '../constants.js'; + +const execAsync = promisify(exec); + +export class EnvironmentDetector { + async detect(): Promise { + const info: EnvironmentInfo = { + os: os.platform(), + arch: os.arch(), + }; + + // Check Node.js + try { + const { stdout: nodeVersion } = await execAsync('node --version'); + const { stdout: nodePath } = await execAsync( + process.platform === 'win32' ? 'where node' : 'which node' + ); + info.node = { + version: nodeVersion.trim(), + path: nodePath.trim().split('\n')[0], + }; + } catch { + // Node not found + } + + // Check npm + try { + const { stdout: npmVersion } = await execAsync('npm --version'); + const { stdout: npmPath } = await execAsync( + process.platform === 'win32' ? 'where npm' : 'which npm' + ); + info.npm = { + version: npmVersion.trim(), + path: npmPath.trim().split('\n')[0], + }; + } catch { + // npm not found + } + + // Check Rust + try { + const { stdout: rustVersion } = await execAsync('rustc --version'); + const { stdout: rustPath } = await execAsync( + process.platform === 'win32' ? 'where rustc' : 'which rustc' + ); + info.rust = { + version: rustVersion.trim(), + path: rustPath.trim().split('\n')[0], + }; + } catch { + // Rust not found + } + + // Check Cargo + try { + const { stdout: cargoVersion } = await execAsync('cargo --version'); + const { stdout: cargoPath } = await execAsync( + process.platform === 'win32' ? 'where cargo' : 'which cargo' + ); + info.cargo = { + version: cargoVersion.trim(), + path: cargoPath.trim().split('\n')[0], + }; + } catch { + // Cargo not found + } + + // Check pubky-testnet + try { + const { stdout: testnetPath } = await execAsync( + process.platform === 'win32' ? 'where pubky-testnet' : 'which pubky-testnet' + ); + info.pubkyTestnet = { + installed: true, + path: testnetPath.trim().split('\n')[0], + }; + } catch { + info.pubkyTestnet = { + installed: false, + }; + } + + return info; + } + + async analyzeProject(projectPath: string): Promise { + const analysis: ProjectAnalysis = { + type: 'unknown', + hasPackageJson: false, + hasCargoToml: false, + hasPubkyDependency: false, + dependencies: [], + devDependencies: [], + }; + + // Check for package.json + const packageJsonPath = path.join(projectPath, 'package.json'); + try { + const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8')); + analysis.hasPackageJson = true; + analysis.type = packageJson.devDependencies?.typescript + ? PROJECT_TYPES.TYPESCRIPT + : PROJECT_TYPES.JAVASCRIPT; + + analysis.dependencies = Object.keys(packageJson.dependencies || {}); + analysis.devDependencies = Object.keys(packageJson.devDependencies || {}); + + if (analysis.dependencies.includes('@synonymdev/pubky')) { + analysis.hasPubkyDependency = true; + } + + // Detect framework + switch (true) { + case analysis.dependencies.includes('react') || analysis.dependencies.includes('next'): + analysis.framework = FRAMEWORKS.REACT; + break; + case analysis.dependencies.includes('vue'): + analysis.framework = FRAMEWORKS.VUE; + break; + case analysis.dependencies.includes('svelte'): + analysis.framework = FRAMEWORKS.SVELTE; + break; + } + } catch { + // No package.json or invalid + } + + // Check for Cargo.toml + const cargoTomlPath = path.join(projectPath, 'Cargo.toml'); + try { + const cargoToml = await fs.readFile(cargoTomlPath, 'utf-8'); + analysis.hasCargoToml = true; + analysis.type = PROJECT_TYPES.RUST; + + if (cargoToml.includes('pubky') || cargoToml.includes('pubky-sdk')) { + analysis.hasPubkyDependency = true; + } + + // Extract dependencies + const depsMatch = cargoToml.match(/\[dependencies\]([\s\S]*?)(?=\[|$)/); + if (depsMatch) { + const deps = depsMatch[1].match(/^(\w+(-\w+)*)\s*=/gm); + if (deps) { + analysis.dependencies = deps.map(d => d.split('=')[0].trim()); + } + } + } catch { + // No Cargo.toml or invalid + } + + return analysis; + } + + async installPubkyTestnet(): Promise { + try { + const { stdout, stderr } = await execAsync('cargo install pubky-testnet', { + timeout: 300000, // 5 minutes timeout + }); + return `Successfully installed pubky-testnet:\n${stdout}\n${stderr}`; + } catch (error: any) { + throw new Error(`Failed to install pubky-testnet: ${error.message}`); + } + } + + async installNpmPackage( + projectPath: string, + packageName: string = '@synonymdev/pubky' + ): Promise { + try { + const { stdout, stderr } = await execAsync(`npm install ${packageName}`, { + cwd: projectPath, + timeout: 120000, // 2 minutes timeout + }); + return `Successfully installed ${packageName}:\n${stdout}\n${stderr}`; + } catch (error: any) { + throw new Error(`Failed to install ${packageName}: ${error.message}`); + } + } + + async addCargodependency( + projectPath: string, + dependency: string = 'pubky', + version?: string + ): Promise { + const cargoTomlPath = path.join(projectPath, 'Cargo.toml'); + + try { + let cargoToml = await fs.readFile(cargoTomlPath, 'utf-8'); + + // Find the dependencies section or create it + const depString = version ? `${dependency} = "${version}"` : `${dependency} = "0.4"`; + + if (cargoToml.includes('[dependencies]')) { + // Add after [dependencies] + cargoToml = cargoToml.replace('[dependencies]', `[dependencies]\n${depString}`); + } else { + // Add new dependencies section + cargoToml += `\n\n[dependencies]\n${depString}\n`; + } + + await fs.writeFile(cargoTomlPath, cargoToml, 'utf-8'); + return `Successfully added ${dependency} to Cargo.toml`; + } catch (error: any) { + throw new Error(`Failed to add dependency to Cargo.toml: ${error.message}`); + } + } +} diff --git a/pubky-mcp-server/src/utils/file-reader.ts b/pubky-mcp-server/src/utils/file-reader.ts new file mode 100644 index 0000000..81250b2 --- /dev/null +++ b/pubky-mcp-server/src/utils/file-reader.ts @@ -0,0 +1,244 @@ +/** + * Utilities for reading files from the Pubky ecosystem repositories + */ + +import * as fs from 'fs/promises'; +import * as path from 'path'; +import type { PubkyCorePaths, PkarrPaths, PkdnsPaths, NexusPaths } from '../types.js'; +import { SEARCH_FILE_EXTENSIONS, IGNORED_DIRECTORIES } from '../constants.js'; + +export class FileReader { + private paths: PubkyCorePaths; + private pkarrPaths: PkarrPaths | null = null; + private pkdnsPaths: PkdnsPaths | null = null; + private nexusPaths: NexusPaths | null = null; + + constructor( + pubkyCoreRoot: string, + pkarrRoot?: string, + pkdnsRoot?: string, + nexusRoot?: string + ) { + this.paths = { + root: pubkyCoreRoot, + docs: path.join(pubkyCoreRoot, 'docs'), + examples: path.join(pubkyCoreRoot, 'examples'), + examplesRust: path.join(pubkyCoreRoot, 'examples', 'rust'), + examplesJs: path.join(pubkyCoreRoot, 'examples', 'javascript'), + }; + + if (pkarrRoot) { + this.pkarrPaths = { + root: pkarrRoot, + design: path.join(pkarrRoot, 'design'), + examples: path.join(pkarrRoot, 'pkarr', 'examples'), + bindingsJs: path.join(pkarrRoot, 'bindings', 'js'), + relay: path.join(pkarrRoot, 'relay'), + }; + } + + if (pkdnsRoot) { + this.pkdnsPaths = { + root: pkdnsRoot, + docs: path.join(pkdnsRoot, 'docs'), + cli: path.join(pkdnsRoot, 'cli'), + serverConfig: path.join(pkdnsRoot, 'server', 'config.sample.toml'), + }; + } + + if (nexusRoot) { + this.nexusPaths = { + root: nexusRoot, + docs: path.join(nexusRoot, 'docs'), + examples: path.join(nexusRoot, 'examples'), + componentReadmes: { + common: path.join(nexusRoot, 'nexus-common', 'README.md'), + watcher: path.join(nexusRoot, 'nexus-watcher', 'README.md'), + webapi: path.join(nexusRoot, 'nexus-webapi', 'README.md'), + }, + }; + } + } + + async readFile(filePath: string): Promise { + try { + return await fs.readFile(filePath, 'utf-8'); + } catch (error) { + throw new Error(`Failed to read file ${filePath}: ${error}`); + } + } + + async fileExists(filePath: string): Promise { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } + } + + async readDocFile(relativePath: string): Promise { + const fullPath = path.join(this.paths.docs, relativePath); + return this.readFile(fullPath); + } + + async readExampleFile(language: 'rust' | 'javascript', relativePath: string): Promise { + const basePath = language === 'rust' ? this.paths.examplesRust : this.paths.examplesJs; + const fullPath = path.join(basePath, relativePath); + return this.readFile(fullPath); + } + + async readPkarrDesignDoc(docName: string): Promise { + if (!this.pkarrPaths) { + throw new Error('Pkarr paths not initialized'); + } + const fullPath = path.join(this.pkarrPaths.design, `${docName}.md`); + return this.readFile(fullPath); + } + + async readPkarrExample(exampleName: string): Promise { + if (!this.pkarrPaths) { + throw new Error('Pkarr paths not initialized'); + } + const fullPath = path.join(this.pkarrPaths.examples, `${exampleName}.rs`); + return this.readFile(fullPath); + } + + async readPkarrJsBindings(relativePath: string): Promise { + if (!this.pkarrPaths) { + throw new Error('Pkarr paths not initialized'); + } + const fullPath = path.join(this.pkarrPaths.bindingsJs, relativePath); + return this.readFile(fullPath); + } + + async readPkarrRelayConfig(): Promise { + if (!this.pkarrPaths) { + throw new Error('Pkarr paths not initialized'); + } + const fullPath = path.join(this.pkarrPaths.relay, 'src', 'config.example.toml'); + return this.readFile(fullPath); + } + + async readPkarrFile(relativePath: string): Promise { + if (!this.pkarrPaths) { + throw new Error('Pkarr paths not initialized'); + } + const fullPath = path.join(this.pkarrPaths.root, relativePath); + return this.readFile(fullPath); + } + + async listDirectory(dirPath: string): Promise { + try { + const entries = await fs.readdir(dirPath, { withFileTypes: true }); + return entries.map(entry => entry.name); + } catch (error) { + throw new Error(`Failed to list directory ${dirPath}: ${error}`); + } + } + + async searchFiles( + dirPath: string, + query: string + ): Promise> { + const results: Array<{ path: string; matches: string[] }> = []; + const queryLower = query.toLowerCase(); + + async function searchRecursive(currentPath: string) { + const entries = await fs.readdir(currentPath, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(currentPath, entry.name); + + switch (true) { + case entry.isDirectory(): { + // Skip node_modules, .git, etc. + if (!IGNORED_DIRECTORIES.includes(entry.name as any)) { + await searchRecursive(fullPath); + } + break; + } + case entry.isFile() && SEARCH_FILE_EXTENSIONS.some(ext => entry.name.endsWith(ext)): { + try { + const content = await fs.readFile(fullPath, 'utf-8'); + const lines = content.split('\n'); + const matches: string[] = []; + + lines.forEach((line, index) => { + if (line.toLowerCase().includes(queryLower)) { + matches.push(`Line ${index + 1}: ${line.trim()}`); + } + }); + + if (matches.length > 0) { + results.push({ path: fullPath, matches: matches.slice(0, 5) }); // Limit to 5 matches per file + } + } catch { + // Skip files that can't be read + } + break; + } + } + } + } + + await searchRecursive(dirPath); + return results; + } + + getPaths(): PubkyCorePaths { + return this.paths; + } + + getPkarrPaths(): PkarrPaths | null { + return this.pkarrPaths; + } + + async readPkdnsDoc(docName: string): Promise { + if (!this.pkdnsPaths) { + throw new Error('Pkdns paths not initialized'); + } + const fullPath = path.join(this.pkdnsPaths.docs, `${docName}.md`); + return this.readFile(fullPath); + } + + async readPkdnsFile(relativePath: string): Promise { + if (!this.pkdnsPaths) { + throw new Error('Pkdns paths not initialized'); + } + const fullPath = path.join(this.pkdnsPaths.root, relativePath); + return this.readFile(fullPath); + } + + getPkdnsPaths(): PkdnsPaths | null { + return this.pkdnsPaths; + } + + async readNexusDoc(docName: string): Promise { + if (!this.nexusPaths) { + throw new Error('Nexus paths not initialized'); + } + const fullPath = path.join(this.nexusPaths.docs, docName); + return this.readFile(fullPath); + } + + async readNexusComponentReadme(component: 'common' | 'watcher' | 'webapi'): Promise { + if (!this.nexusPaths) { + throw new Error('Nexus paths not initialized'); + } + const fullPath = this.nexusPaths.componentReadmes[component]; + return this.readFile(fullPath); + } + + async readNexusFile(relativePath: string): Promise { + if (!this.nexusPaths) { + throw new Error('Nexus paths not initialized'); + } + const fullPath = path.join(this.nexusPaths.root, relativePath); + return this.readFile(fullPath); + } + + getNexusPaths(): NexusPaths | null { + return this.nexusPaths; + } +} diff --git a/pubky-mcp-server/src/utils/nexus-api.ts b/pubky-mcp-server/src/utils/nexus-api.ts new file mode 100644 index 0000000..345b9f0 --- /dev/null +++ b/pubky-mcp-server/src/utils/nexus-api.ts @@ -0,0 +1,309 @@ +/** + * Nexus API Parser - Utilities for parsing and exposing nexus-webapi.json OpenAPI specification + */ + +import * as fs from 'fs/promises'; +import * as path from 'path'; + +export interface NexusEndpoint { + path: string; + method: string; + operationId: string; + description: string; + parameters: any[]; + requestBody?: any; + responses: any; + tags: string[]; +} + +export interface NexusSchema { + name: string; + schema: any; + description?: string; +} + +export class NexusApiParser { + private spec: any = null; + private workspaceRoot: string; + + constructor(workspaceRoot: string) { + this.workspaceRoot = workspaceRoot; + } + + async loadSpec(): Promise { + if (this.spec) return; + + const specPath = path.join(this.workspaceRoot, 'nexus-webapi.json'); + const content = await fs.readFile(specPath, 'utf-8'); + this.spec = JSON.parse(content); + } + + async getOverview(): Promise { + await this.loadSpec(); + + let output = `# Nexus Web API\n\n`; + output += `**Version**: ${this.spec.info.version}\n`; + output += `**Title**: ${this.spec.info.title}\n\n`; + output += `${this.spec.info.description}\n\n`; + output += `**License**: ${this.spec.info.license.name}\n\n`; + + output += `## API Categories\n\n`; + const categories = this.getEndpointCategories(); + for (const [category, count] of Object.entries(categories)) { + output += `- **${category}**: ${count} endpoints\n`; + } + + return output; + } + + async getEndpointsByCategory(category: string): Promise { + await this.loadSpec(); + + const endpoints = this.extractEndpoints().filter(ep => + ep.tags.some(tag => tag.toLowerCase() === category.toLowerCase()) + ); + + if (endpoints.length === 0) { + return `No endpoints found for category: ${category}`; + } + + let output = `# ${category} Endpoints\n\n`; + + for (const endpoint of endpoints) { + output += `## ${endpoint.method.toUpperCase()} ${endpoint.path}\n\n`; + output += `**Operation**: \`${endpoint.operationId}\`\n\n`; + output += `${endpoint.description}\n\n`; + + if (endpoint.parameters.length > 0) { + output += `### Parameters\n\n`; + for (const param of endpoint.parameters) { + const required = param.required ? '(required)' : '(optional)'; + output += `- **${param.name}** ${required}: ${param.description || 'No description'}\n`; + if (param.schema) { + output += ` - Type: \`${param.schema.type || 'any'}\`\n`; + if (param.schema.enum) { + output += ` - Values: ${param.schema.enum.map((v: any) => `\`${v}\``).join(', ')}\n`; + } + } + } + output += `\n`; + } + + if (endpoint.requestBody) { + output += `### Request Body\n\n`; + const content = endpoint.requestBody.content?.['application/json']; + if (content?.schema?.$ref) { + const schemaName = content.schema.$ref.split('/').pop(); + output += `Schema: \`${schemaName}\`\n\n`; + } + } + + output += `### Responses\n\n`; + for (const [code, response] of Object.entries(endpoint.responses)) { + output += `- **${code}**: ${(response as any).description}\n`; + } + output += `\n---\n\n`; + } + + return output; + } + + async getSchemas(): Promise { + await this.loadSpec(); + + let output = `# Nexus API Data Schemas\n\n`; + + const schemas = this.spec.components?.schemas || {}; + const schemaNames = Object.keys(schemas).sort(); + + output += `## Available Schemas\n\n`; + for (const name of schemaNames) { + const schema = schemas[name]; + const desc = schema.description || 'No description'; + output += `- **${name}**: ${desc}\n`; + } + + output += `\n## Schema Details\n\n`; + + for (const name of schemaNames) { + const schema = schemas[name]; + output += `### ${name}\n\n`; + + if (schema.description) { + output += `${schema.description}\n\n`; + } + + if (schema.type === 'object' && schema.properties) { + output += `**Properties**:\n\n`; + for (const [propName, propSchema] of Object.entries(schema.properties)) { + const prop: any = propSchema; + const required = schema.required?.includes(propName) ? ' (required)' : ''; + const type = prop.type || (prop.$ref ? prop.$ref.split('/').pop() : 'unknown'); + output += `- \`${propName}\`${required}: ${type}`; + if (prop.description) { + output += ` - ${prop.description}`; + } + output += `\n`; + } + output += `\n`; + } else if (schema.type === 'array') { + output += `**Type**: Array\n\n`; + if (schema.items?.$ref) { + output += `**Items**: \`${schema.items.$ref.split('/').pop()}\`\n\n`; + } + } else if (schema.enum) { + output += `**Type**: Enum\n\n`; + output += `**Values**: ${schema.enum.map((v: any) => `\`${v}\``).join(', ')}\n\n`; + } else if (schema.oneOf) { + output += `**Type**: One of multiple types\n\n`; + } + + output += `---\n\n`; + } + + return output; + } + + async generateEndpointExample(operationId: string): Promise { + await this.loadSpec(); + + const endpoints = this.extractEndpoints(); + const endpoint = endpoints.find(ep => ep.operationId === operationId); + + if (!endpoint) { + return `Endpoint with operation ID "${operationId}" not found`; + } + + let output = `# ${endpoint.operationId} Example\n\n`; + output += `## Endpoint\n\n`; + output += `\`${endpoint.method.toUpperCase()} ${endpoint.path}\`\n\n`; + output += `${endpoint.description}\n\n`; + + // JavaScript example + output += `## JavaScript Example\n\n\`\`\`javascript\n`; + output += `const NEXUS_API_URL = 'https://nexus.example.com';\n\n`; + output += `async function ${this.toCamelCase(operationId)}(`; + + const pathParams = endpoint.parameters.filter((p: any) => p.in === 'path'); + const queryParams = endpoint.parameters.filter((p: any) => p.in === 'query'); + + const paramNames = pathParams.map((p: any) => p.name); + if (queryParams.length > 0) { + paramNames.push('options = {}'); + } + + output += paramNames.join(', '); + output += `) {\n`; + + // Build URL + let urlPath = endpoint.path; + for (const param of pathParams) { + urlPath = urlPath.replace(`{${param.name}}`, `\${${param.name}}`); + } + output += ` let url = \`\${NEXUS_API_URL}${urlPath}\`;\n\n`; + + if (queryParams.length > 0) { + output += ` // Add query parameters\n`; + output += ` const params = new URLSearchParams();\n`; + for (const param of queryParams) { + output += ` if (options.${param.name} !== undefined) params.append('${param.name}', options.${param.name});\n`; + } + output += ` if (params.toString()) url += '?' + params.toString();\n\n`; + } + + output += ` const response = await fetch(url`; + if (endpoint.requestBody) { + output += `, {\n method: '${endpoint.method.toUpperCase()}',\n`; + output += ` headers: { 'Content-Type': 'application/json' },\n`; + output += ` body: JSON.stringify(data)\n }`; + } + output += `);\n\n`; + output += ` if (!response.ok) throw new Error(\`HTTP error! status: \${response.status}\`);\n`; + output += ` return await response.json();\n`; + output += `}\n\n`; + + // Usage example + output += `// Usage\n`; + if (pathParams.length > 0) { + const exampleParams = pathParams.map((p: any) => `'example_${p.name}'`).join(', '); + output += `const result = await ${this.toCamelCase(operationId)}(${exampleParams});\n`; + } else { + output += `const result = await ${this.toCamelCase(operationId)}();\n`; + } + output += `console.log(result);\n`; + output += `\`\`\`\n`; + + return output; + } + + private extractEndpoints(): NexusEndpoint[] { + const endpoints: NexusEndpoint[] = []; + + for (const [path, pathItem] of Object.entries(this.spec.paths || {})) { + for (const [method, operation] of Object.entries(pathItem as any)) { + if (['get', 'post', 'put', 'delete', 'patch'].includes(method)) { + const op = operation as any; + endpoints.push({ + path, + method, + operationId: op.operationId, + description: op.description || op.summary || '', + parameters: op.parameters || [], + requestBody: op.requestBody, + responses: op.responses || {}, + tags: op.tags || [], + }); + } + } + } + + return endpoints; + } + + private getEndpointCategories(): Record { + const endpoints = this.extractEndpoints(); + const categories: Record = {}; + + for (const endpoint of endpoints) { + for (const tag of endpoint.tags) { + categories[tag] = (categories[tag] || 0) + 1; + } + } + + return categories; + } + + private toCamelCase(str: string): string { + return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); + } + + async searchEndpoints(query: string): Promise { + await this.loadSpec(); + + const queryLower = query.toLowerCase(); + const endpoints = this.extractEndpoints(); + const matches = endpoints.filter( + ep => + ep.operationId.toLowerCase().includes(queryLower) || + ep.description.toLowerCase().includes(queryLower) || + ep.path.toLowerCase().includes(queryLower) + ); + + if (matches.length === 0) { + return `No endpoints found matching: ${query}`; + } + + let output = `# Search Results for "${query}"\n\n`; + output += `Found ${matches.length} matching endpoint(s):\n\n`; + + for (const endpoint of matches) { + output += `## ${endpoint.method.toUpperCase()} ${endpoint.path}\n`; + output += `- **Operation**: ${endpoint.operationId}\n`; + output += `- **Description**: ${endpoint.description}\n`; + output += `- **Tags**: ${endpoint.tags.join(', ')}\n\n`; + } + + return output; + } +} + diff --git a/pubky-mcp-server/src/utils/pkarr-relay.ts b/pubky-mcp-server/src/utils/pkarr-relay.ts new file mode 100644 index 0000000..43f15c4 --- /dev/null +++ b/pubky-mcp-server/src/utils/pkarr-relay.ts @@ -0,0 +1,209 @@ +/** + * Utilities for managing Pkarr relay + */ + +import { spawn, exec, ChildProcess } from 'child_process'; +import { promisify } from 'util'; +import { writeFile, mkdir } from 'fs/promises'; +import { join } from 'path'; +import { tmpdir } from 'os'; +import type { PkarrRelayInfo, PkarrRelayConfig } from '../types.js'; +import { PKARR_RELAY_DEFAULT_PORT } from '../constants.js'; + +const execAsync = promisify(exec); + +let relayProcess: ChildProcess | null = null; +let currentConfig: PkarrRelayConfig = {}; + +export class PkarrRelayManager { + private pkarrRoot: string; + + constructor(pkarrRoot: string) { + this.pkarrRoot = pkarrRoot; + } + + async getInfo(): Promise { + const running = await this.isRunning(); + const port = currentConfig.port || PKARR_RELAY_DEFAULT_PORT; + + return { + running, + port: running ? port : undefined, + cacheLocation: currentConfig.cachePath, + url: running ? `http://localhost:${port}` : undefined, + testnet: currentConfig.testnet, + }; + } + + async isRunning(): Promise { + try { + const port = currentConfig.port || PKARR_RELAY_DEFAULT_PORT; + // Try to fetch from the relay + const response = await fetch(`http://localhost:${port}/`, { + signal: AbortSignal.timeout(1000), + }); + // Relay may return 404 for root path, which is fine + return response.status === 404 || response.ok; + } catch { + return false; + } + } + + async start(config: PkarrRelayConfig = {}): Promise { + if (await this.isRunning()) { + return 'Pkarr relay is already running'; + } + + // Store config + currentConfig = config; + + const port = config.port || PKARR_RELAY_DEFAULT_PORT; + const cachePath = config.cachePath || join(tmpdir(), 'pkarr-cache'); + + // Create config file + const configContent = this.generateConfigFile(config); + const configDir = join(tmpdir(), 'pkarr-relay-config'); + await mkdir(configDir, { recursive: true }); + const configFilePath = join(configDir, 'config.toml'); + await writeFile(configFilePath, configContent); + + // Try to use installed binary first, then fall back to cargo run + let command: string; + let args: string[]; + let cwd: string | undefined; + + try { + await execAsync('which pkarr-relay'); + command = 'pkarr-relay'; + args = ['--config', configFilePath]; + if (config.testnet) { + args.push('--testnet'); + } + cwd = undefined; + } catch { + // Binary not found, use cargo run from the pkarr repository + command = 'cargo'; + args = ['run', '--release', '--manifest-path', join(this.pkarrRoot, 'relay/Cargo.toml')]; + if (config.testnet) { + args.push('--', '--testnet'); + } else { + args.push('--', '--config', configFilePath); + } + cwd = this.pkarrRoot; + } + + return new Promise((resolve, reject) => { + relayProcess = spawn(command, args, { + cwd, + stdio: ['ignore', 'pipe', 'pipe'], + detached: false, + }); + + let output = ''; + + relayProcess.stdout?.on('data', data => { + output += data.toString(); + }); + + relayProcess.stderr?.on('data', data => { + output += data.toString(); + }); + + // Wait for the relay to start + setTimeout(async () => { + if (await this.isRunning()) { + const mode = config.testnet ? 'testnet mode' : 'production mode'; + resolve( + `Pkarr relay started successfully in ${mode}!\n\nURL: http://localhost:${port}\nCache: ${cachePath}\n\nThe relay will keep running in the background.` + ); + } else { + reject(new Error(`Failed to start Pkarr relay. Output:\n${output}`)); + } + }, 3000); + + relayProcess.on('error', error => { + reject(new Error(`Failed to start Pkarr relay: ${error.message}`)); + }); + }); + } + + async stop(): Promise { + if (!(await this.isRunning())) { + return 'Pkarr relay is not running'; + } + + if (relayProcess) { + relayProcess.kill('SIGTERM'); + relayProcess = null; + currentConfig = {}; + return 'Pkarr relay stopped successfully'; + } + + // Try to find and kill the process + try { + if (process.platform === 'win32') { + await execAsync('taskkill /F /IM pkarr-relay.exe'); + } else { + await execAsync('pkill -f pkarr-relay'); + } + currentConfig = {}; + return 'Pkarr relay stopped successfully'; + } catch (error) { + return `Could not stop Pkarr relay: ${error}`; + } + } + + async restart(config?: PkarrRelayConfig): Promise { + const stopResult = await this.stop(); + await new Promise(resolve => setTimeout(resolve, 1000)); // Wait a second + const startResult = await this.start(config || currentConfig); + return `${stopResult}\n${startResult}`; + } + + private generateConfigFile(config: PkarrRelayConfig): string { + const port = config.port || PKARR_RELAY_DEFAULT_PORT; + const cachePath = config.cachePath || join(tmpdir(), 'pkarr-cache'); + const cacheSize = config.cacheSize || 1_000_000; + const minimumTtl = config.minimumTtl || 300; + const maximumTtl = config.maximumTtl || 86400; + + let configContent = `# HTTP server configurations. +[http] +# The port number to run the HTTP server on. +port = ${port} + +# Internal Mainline node configurations +[mainline] +# Port to run the internal Mainline DHT node on. +port = ${port} + +# Cache settings +[cache] +# Set the path for the cache storage. +path = "${cachePath}" +# Maximum number of SignedPackets to store, before evicting the oldest packets. +size = ${cacheSize} + +# Minimum TTL before attempting to lookup a more recent version of a SignedPacket +minimum_ttl = ${minimumTtl} +# Maximum TTL before attempting to lookup a more recent version of a SignedPacket +maximum_ttl = ${maximumTtl} +`; + + if (config.rateLimiter) { + configContent += ` +# Ip rate limiting configurations. +[rate_limiter] +# Set to true if you are running this relay behind a reverse proxy +behind_proxy = ${config.rateLimiter.behindProxy} +# Maximum number of requests per second. +burst_size = ${config.rateLimiter.burstSize} +# Number of seconds after which one request of the quota is replenished. +per_second = ${config.rateLimiter.perSecond} +`; + } + + return configContent; + } +} + diff --git a/pubky-mcp-server/src/utils/templates.ts b/pubky-mcp-server/src/utils/templates.ts new file mode 100644 index 0000000..f387dd8 --- /dev/null +++ b/pubky-mcp-server/src/utils/templates.ts @@ -0,0 +1,678 @@ +/** + * Code generation templates for Pubky applications + */ + +import type { CodeTemplate } from '../types.js'; + +export const templates: Record = { + // JavaScript/TypeScript templates + 'js-basic-app': { + name: 'Basic JavaScript Pubky App', + description: 'A minimal Pubky application setup', + language: 'javascript', + dependencies: ['@synonymdev/pubky'], + code: `import { Pubky, Keypair } from '@synonymdev/pubky'; + +// Initialize Pubky client +const pubky = new Pubky(); // or Pubky.testnet() for local development + +// Create a new user with a random keypair +const keypair = Keypair.random(); +const signer = pubky.signer(keypair); + +// Sign up on a homeserver +const homeserverPubkey = 'YOUR_HOMESERVER_PUBLIC_KEY'; +const session = await signer.signup(homeserverPubkey, null); + +// Write data to storage +await session.storage().put('/pub/my-app/data.json', JSON.stringify({ hello: 'world' })); + +// Read data back +const data = await session.storage().get('/pub/my-app/data.json'); +console.log('Stored data:', await data.text()); +`, + }, + + 'js-auth-flow': { + name: 'JavaScript Auth Flow', + description: 'Implement Pubky QR authentication for keyless apps', + language: 'javascript', + dependencies: ['@synonymdev/pubky'], + code: `import { Pubky, Capabilities } from '@synonymdev/pubky'; + +const pubky = new Pubky(); + +// Define required capabilities +const caps = Capabilities.builder() + .readWrite('/pub/my-app/') + .finish(); + +// Start auth flow +const flow = pubky.startAuthFlow(caps); + +// Display QR code to user +console.log('Scan this QR code with your Pubky authenticator:'); +console.log(flow.authorizationUrl()); + +// Wait for user approval +const session = await flow.awaitApproval(); + +console.log('Authentication successful!'); +console.log('User public key:', session.publicKey()); + +// Now you can use the session +await session.storage().put('/pub/my-app/profile.json', JSON.stringify({ + name: 'User', + createdAt: new Date().toISOString() +})); +`, + }, + + 'js-public-read': { + name: 'JavaScript Public Storage Read', + description: 'Read public data from any Pubky user', + language: 'javascript', + dependencies: ['@synonymdev/pubky'], + code: `import { Pubky } from '@synonymdev/pubky'; + +const pubky = new Pubky(); +const publicStorage = pubky.publicStorage(); + +// Read a specific file +const userPubkey = 'USER_PUBLIC_KEY'; +const file = await publicStorage.get(\`pubky\${userPubkey}/pub/my-app/profile.json\`); +const profile = JSON.parse(await file.text()); +console.log('User profile:', profile); + +// List files in a directory +const entries = await publicStorage + .list(\`pubky\${userPubkey}/pub/my-app/\`) + .limit(10) + .send(); + +for (const entry of entries) { + console.log('Found:', entry.path); +} +`, + }, + + 'ts-express-app': { + name: 'TypeScript Express App with Pubky', + description: 'Full Express.js backend with Pubky integration', + language: 'typescript', + dependencies: ['@synonymdev/pubky', 'express'], + code: `import express from 'express'; +import { Pubky, Capabilities, PubkySession } from '@synonymdev/pubky'; + +const app = express(); +app.use(express.json()); + +const pubky = new Pubky(); + +// Store active sessions +const sessions = new Map(); + +// Auth endpoint +app.post('/auth/start', async (req, res) => { + const caps = Capabilities.builder() + .readWrite('/pub/my-app/') + .finish(); + + const flow = pubky.startAuthFlow(caps); + + res.json({ + authUrl: flow.authorizationUrl(), + sessionId: flow.clientSecret() // Use as temp session ID + }); + + // Await approval in background + flow.awaitApproval().then(session => { + sessions.set(flow.clientSecret(), session); + }); +}); + +// Check auth status +app.get('/auth/status/:sessionId', (req, res) => { + const session = sessions.get(req.params.sessionId); + if (session) { + res.json({ + authenticated: true, + publicKey: session.publicKey() + }); + } else { + res.json({ authenticated: false }); + } +}); + +// Protected endpoint +app.post('/api/data', async (req, res) => { + const sessionId = req.headers['x-session-id'] as string; + const session = sessions.get(sessionId); + + if (!session) { + return res.status(401).json({ error: 'Not authenticated' }); + } + + // Write to user's storage + await session.storage().put( + '/pub/my-app/data.json', + JSON.stringify(req.body) + ); + + res.json({ success: true }); +}); + +app.listen(3000, () => { + console.log('Server running on http://localhost:3000'); +}); +`, + }, + + // Rust templates + 'rust-basic-app': { + name: 'Basic Rust Pubky App', + description: 'A minimal Rust Pubky application', + language: 'rust', + dependencies: ['pubky = "0.4"', 'tokio = { version = "1", features = ["full"] }'], + code: `use pubky::prelude::*; + +#[tokio::main] +async fn main() -> pubky::Result<()> { + // Initialize Pubky client + let pubky = Pubky::new()?; // or Pubky::testnet() for local development + + // Create a new user with a random keypair + let keypair = Keypair::random(); + let signer = pubky.signer(keypair); + + // Sign up on a homeserver + let homeserver = PublicKey::try_from("YOUR_HOMESERVER_PUBLIC_KEY")?; + let session = signer.signup(&homeserver, None).await?; + + // Write data to storage + session.storage() + .put("/pub/my-app/hello.txt", "Hello, Pubky!") + .await?; + + // Read data back + let response = session.storage() + .get("/pub/my-app/hello.txt") + .await?; + let text = response.text().await?; + + println!("Stored data: {}", text); + + Ok(()) +} +`, + }, + + 'rust-auth-flow': { + name: 'Rust Auth Flow', + description: 'Implement Pubky authentication flow in Rust', + language: 'rust', + dependencies: ['pubky = "0.4"', 'tokio = { version = "1", features = ["full"] }'], + code: `use pubky::prelude::*; + +#[tokio::main] +async fn main() -> pubky::Result<()> { + let pubky = Pubky::new()?; + + // Define required capabilities + let caps = Capabilities::builder() + .read_write("/pub/my-app/") + .finish(); + + // Start auth flow + let flow = pubky.start_auth_flow(&caps)?; + + // Display authorization URL (could be shown as QR code) + println!("Scan this URL with your Pubky authenticator:"); + println!("{}", flow.authorization_url()); + + // Wait for user approval + println!("Waiting for approval..."); + let session = flow.await_approval().await?; + + println!("Authentication successful!"); + println!("User public key: {}", session.info().public_key()); + + // Now you can use the session + session.storage() + .put("/pub/my-app/profile.json", r#"{"name": "User"}"#) + .await?; + + Ok(()) +} +`, + }, + + 'rust-public-read': { + name: 'Rust Public Storage Read', + description: 'Read public data from any Pubky user in Rust', + language: 'rust', + dependencies: ['pubky = "0.4"', 'tokio = { version = "1", features = ["full"] }'], + code: `use pubky::prelude::*; + +#[tokio::main] +async fn main() -> pubky::Result<()> { + let pubky = Pubky::new()?; + let public_storage = pubky.public_storage(); + + // The user's public key you want to read from + let user_pubkey = PublicKey::try_from("USER_PUBLIC_KEY")?; + + // Read a specific file + let file = public_storage + .get(format!("pubky{}/pub/my-app/profile.json", user_pubkey)) + .await?; + let profile = file.text().await?; + println!("User profile: {}", profile); + + // List files in a directory + let entries = public_storage + .list(format!("pubky{}/pub/my-app/", user_pubkey))? + .limit(10) + .send() + .await?; + + for entry in entries { + println!("Found: {}", entry.to_pubky_url()); + } + + Ok(()) +} +`, + }, + + // Pkarr templates + 'pkarr-client-js': { + name: 'Pkarr Client (JavaScript)', + description: 'Basic Pkarr client for publishing and resolving DNS records', + language: 'javascript', + dependencies: ['pkarr'], + code: `const { Client, Keypair, SignedPacket } = require('pkarr'); + +// Initialize client with default relays +const client = new Client(); + +// Generate or load keypair +const keypair = new Keypair(); +const publicKey = keypair.public_key_string(); +console.log('Public Key:', publicKey); + +// Build DNS records +const builder = SignedPacket.builder(); +builder.addTxtRecord('_service', 'myapp=v1.0', 3600); +builder.addARecord('www', '192.168.1.1', 3600); + +// Sign and publish +const packet = builder.buildAndSign(keypair); +await client.publish(packet); + +console.log('Published! Your domain is:', \`https://\${publicKey}\`); + +// Resolve records later +const resolved = await client.resolve(publicKey); +console.log('DNS Records:', resolved.records); +`, + }, + + 'pkarr-client-rust': { + name: 'Pkarr Client (Rust)', + description: 'Basic Pkarr client for publishing and resolving DNS records', + language: 'rust', + dependencies: ['pkarr = "5.0"'], + code: `use pkarr::{Client, Keypair, SignedPacket}; +use simple_dns::{Name, CLASS, ResourceRecord, rdata::TXT}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize client + let client = Client::default(); + + // Generate or load keypair + let keypair = Keypair::random(); + let public_key = keypair.public_key(); + println!("Public Key: {}", public_key); + + // Create signed packet + let mut packet = SignedPacket::new(&keypair)?; + + // Add DNS records + packet.add_answer(ResourceRecord::new( + Name::new_unchecked("_service"), + CLASS::IN, + 3600, + TXT::new().with_string("myapp=v1.0") + )); + + // Publish to DHT + client.publish(&packet).await?; + + println!("Published! Your domain is: https://{}", public_key); + + // Resolve records later + if let Some(resolved) = client.resolve(&public_key.to_string()).await? { + println!("DNS Records: {:?}", resolved); + } + + Ok(()) +} +`, + }, + + 'pkarr-publish-records': { + name: 'Publish DNS Records', + description: 'Publish various DNS record types using Pkarr', + language: 'javascript', + dependencies: ['pkarr'], + code: `const { Client, Keypair, SignedPacket } = require('pkarr'); + +const client = new Client(); +const keypair = new Keypair(); + +// Build comprehensive DNS records +const builder = SignedPacket.builder(); + +// TXT record for service metadata +builder.addTxtRecord('_service', 'app=myapp version=1.0', 3600); + +// A record for IPv4 +builder.addARecord('www', '192.168.1.1', 3600); + +// AAAA record for IPv6 +builder.addAAAARecord('www', '2001:db8::1', 3600); + +// CNAME record for alias +builder.addCnameRecord('api', 'www', 3600); + +// HTTPS record with service parameters +builder.addHttpsRecord('@', 1, '.', 3600, { + port: 443, + ipv4hint: '192.168.1.1', + alpn: ['h2', 'http/1.1'] +}); + +// Sign and publish +const packet = builder.buildAndSign(keypair); +await client.publish(packet); + +console.log('All records published for key:', keypair.public_key_string()); +`, + }, + + 'pkarr-resolve-key': { + name: 'Resolve Pkarr Key', + description: 'Resolve a public key to get DNS records', + language: 'javascript', + dependencies: ['pkarr'], + code: `const { Client } = require('pkarr'); + +async function resolveKey(publicKey) { + const client = new Client(); + + try { + const packet = await client.resolve(publicKey); + + if (!packet) { + console.log('No records found for key:', publicKey); + return null; + } + + console.log('Resolved records for key:', publicKey); + console.log('Timestamp:', new Date(packet.timestampMs)); + console.log('Records:'); + + for (const record of packet.records) { + console.log(\` - \${record.name}: \${record.type} = \${record.data}\`); + } + + return packet; + } catch (error) { + console.error('Resolution failed:', error.message); + return null; + } +} + +// Example usage +const publicKey = 'YOUR_PUBLIC_KEY_HERE'; +await resolveKey(publicKey); +`, + }, + + 'pkarr-keypair-management': { + name: 'Keypair Management', + description: 'Generate, save, and load Pkarr keypairs securely', + language: 'javascript', + dependencies: ['pkarr'], + code: `const { Keypair } = require('pkarr'); +const fs = require('fs').promises; + +// Generate new keypair +function generateKeypair() { + const keypair = new Keypair(); + const publicKey = keypair.public_key_string(); + const secretKey = keypair.secret_key_bytes(); + + return { + publicKey, + secretKey: Buffer.from(secretKey).toString('hex') + }; +} + +// Save keypair securely (use proper encryption in production!) +async function saveKeypair(keys, filepath) { + await fs.writeFile( + filepath, + JSON.stringify(keys, null, 2), + { mode: 0o600 } // Owner read/write only + ); + console.log('Keypair saved to:', filepath); + console.log('Public Key:', keys.publicKey); + console.log('⚠️ Keep the secret key secure!'); +} + +// Load keypair from file +async function loadKeypair(filepath) { + const data = await fs.readFile(filepath, 'utf-8'); + const keys = JSON.parse(data); + + // Recreate keypair from secret key + const secretBytes = Buffer.from(keys.secretKey, 'hex'); + const keypair = Keypair.from_secret_key(secretBytes); + + return keypair; +} + +// Example usage +const keys = generateKeypair(); +await saveKeypair(keys, './pkarr-keys.json'); + +// Later, load the keypair +const loadedKeypair = await loadKeypair('./pkarr-keys.json'); +console.log('Loaded keypair for:', loadedKeypair.public_key_string()); +`, + }, +}; + +export function getTemplate(name: string): CodeTemplate | undefined { + return templates[name]; +} + +export function listTemplates(): Array<{ name: string; description: string; language: string }> { + return Object.entries(templates).map(([key, template]) => ({ + name: key, + description: template.description, + language: template.language, + })); +} + +export function generateScaffold( + projectName: string, + language: 'rust' | 'javascript' | 'typescript', + features: string[] +): { files: Map; instructions: string } { + const files = new Map(); + + if (language === 'rust') { + // Cargo.toml + files.set( + 'Cargo.toml', + `[package] +name = "${projectName}" +version = "0.1.0" +edition = "2021" + +[dependencies] +pubky = "0.4" +tokio = { version = "1", features = ["full"] } +anyhow = "1.0" +${features.includes('json') ? 'serde = { version = "1.0", features = ["derive"] }\nserde_json = "1.0"' : ''} +` + ); + + // src/main.rs + const mainCode = features.includes('auth') + ? templates['rust-auth-flow'].code + : templates['rust-basic-app'].code; + files.set('src/main.rs', mainCode); + + // README.md + files.set( + 'README.md', + `# ${projectName} + +A Pubky application built with Rust. + +## Getting Started + +1. Install Rust: https://rustup.rs/ +2. Run the application: + \`\`\`bash + cargo run + \`\`\` + +## Development with Testnet + +Start a local testnet: +\`\`\`bash +cargo install pubky-testnet +pubky-testnet +\`\`\` + +Then update your code to use \`Pubky::testnet()\` instead of \`Pubky::new()\`. +` + ); + + return { + files, + instructions: `Rust project scaffold created! + +Next steps: +1. cd ${projectName} +2. cargo build +3. Update YOUR_HOMESERVER_PUBLIC_KEY in src/main.rs +4. cargo run + +For local development, start testnet with: pubky-testnet +`, + }; + } else { + // package.json + const packageJson = { + name: projectName, + version: '1.0.0', + type: 'module', + scripts: { + start: language === 'typescript' ? 'tsx src/index.ts' : 'node src/index.js', + dev: language === 'typescript' ? 'tsx watch src/index.ts' : 'node --watch src/index.js', + }, + dependencies: { + '@synonymdev/pubky': '^0.4.0', + }, + devDependencies: + language === 'typescript' + ? { + typescript: '^5.0.0', + tsx: '^4.0.0', + '@types/node': '^20.0.0', + } + : {}, + }; + + files.set('package.json', JSON.stringify(packageJson, null, 2)); + + if (language === 'typescript') { + files.set( + 'tsconfig.json', + `{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "lib": ["ES2022"], + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"] +} +` + ); + } + + // Main file + const mainCode = features.includes('auth') + ? templates['js-auth-flow'].code + : templates['js-basic-app'].code; + const mainFile = language === 'typescript' ? 'src/index.ts' : 'src/index.js'; + files.set(mainFile, mainCode); + + // README.md + files.set( + 'README.md', + `# ${projectName} + +A Pubky application built with ${language === 'typescript' ? 'TypeScript' : 'JavaScript'}. + +## Getting Started + +1. Install dependencies: + \`\`\`bash + npm install + \`\`\` + +2. Run the application: + \`\`\`bash + npm start + \`\`\` + +## Development with Testnet + +Start a local testnet: +\`\`\`bash +npx pubky-testnet +\`\`\` + +Then update your code to use \`Pubky.testnet()\` instead of \`new Pubky()\`. +` + ); + + return { + files, + instructions: `${language === 'typescript' ? 'TypeScript' : 'JavaScript'} project scaffold created! + +Next steps: +1. cd ${projectName} +2. npm install +3. Update YOUR_HOMESERVER_PUBLIC_KEY in ${mainFile} +4. npm start + +For local development, start testnet with: npx pubky-testnet +`, + }; + } +} diff --git a/pubky-mcp-server/src/utils/testnet.ts b/pubky-mcp-server/src/utils/testnet.ts new file mode 100644 index 0000000..29bca7e --- /dev/null +++ b/pubky-mcp-server/src/utils/testnet.ts @@ -0,0 +1,139 @@ +/** + * Utilities for managing Pubky testnet + */ + +import { spawn, exec, ChildProcess } from 'child_process'; +import { promisify } from 'util'; +import type { TestnetInfo } from '../types.js'; + +const execAsync = promisify(exec); + +// Hardcoded testnet configuration from pubky-testnet +const TESTNET_CONFIG = { + homeserverPublicKey: '8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo', + ports: { + dht: 6881, + pkarrRelay: 15411, + httpRelay: 15412, + homeserver: 15411, // Same as pkarr relay + adminServer: 6288, + }, +}; + +let testnetProcess: ChildProcess | null = null; + +export class TestnetManager { + async getInfo(): Promise { + const isRunning = await this.isRunning(); + + return { + isRunning, + homeserverPublicKey: TESTNET_CONFIG.homeserverPublicKey, + ports: TESTNET_CONFIG.ports, + urls: { + homeserver: `http://localhost:${TESTNET_CONFIG.ports.homeserver}`, + admin: `http://localhost:${TESTNET_CONFIG.ports.adminServer}`, + httpRelay: `http://localhost:${TESTNET_CONFIG.ports.httpRelay}`, + }, + }; + } + + async isRunning(): Promise { + try { + // Check if testnet is responding on HTTP relay port + const response = await fetch(`http://localhost:${TESTNET_CONFIG.ports.httpRelay}/`, { + signal: AbortSignal.timeout(1000), + }); + return response.ok || response.status === 404; // 404 is ok, means server is running + } catch { + return false; + } + } + + async start(pubkyCoreRoot: string): Promise { + if (await this.isRunning()) { + return 'Testnet is already running'; + } + + // Try to use installed binary first, then fall back to cargo run + let command: string; + let args: string[]; + let cwd: string | undefined; + + try { + await execAsync('which pubky-testnet'); + command = 'pubky-testnet'; + args = []; + cwd = undefined; + } catch { + // Binary not found, use cargo run + command = 'cargo'; + args = ['run', '-p', 'pubky-testnet']; + cwd = pubkyCoreRoot; + } + + return new Promise((resolve, reject) => { + testnetProcess = spawn(command, args, { + cwd, + stdio: ['ignore', 'pipe', 'pipe'], + detached: false, + }); + + let output = ''; + + testnetProcess.stdout?.on('data', data => { + output += data.toString(); + }); + + testnetProcess.stderr?.on('data', data => { + output += data.toString(); + }); + + // Wait a bit for the server to start + setTimeout(async () => { + if (await this.isRunning()) { + resolve( + `Testnet started successfully!\n\nHomeserver: http://localhost:${TESTNET_CONFIG.ports.homeserver}\nHTTP Relay: http://localhost:${TESTNET_CONFIG.ports.httpRelay}\nPublic Key: ${TESTNET_CONFIG.homeserverPublicKey}\n\nThe testnet will keep running in the background.` + ); + } else { + reject(new Error(`Failed to start testnet. Output:\n${output}`)); + } + }, 3000); + + testnetProcess.on('error', error => { + reject(new Error(`Failed to start testnet: ${error.message}`)); + }); + }); + } + + async stop(): Promise { + if (!(await this.isRunning())) { + return 'Testnet is not running'; + } + + if (testnetProcess) { + testnetProcess.kill('SIGTERM'); + testnetProcess = null; + return 'Testnet stopped successfully'; + } + + // Try to find and kill the process + try { + if (process.platform === 'win32') { + await execAsync('taskkill /F /IM pubky-testnet.exe'); + } else { + await execAsync('pkill -f pubky-testnet'); + } + return 'Testnet stopped successfully'; + } catch (error) { + return `Could not stop testnet: ${error}`; + } + } + + async restart(pubkyCoreRoot: string): Promise { + const stopResult = await this.stop(); + await new Promise(resolve => setTimeout(resolve, 1000)); // Wait a second + const startResult = await this.start(pubkyCoreRoot); + return `${stopResult}\n${startResult}`; + } +} diff --git a/pubky-mcp-server/tsconfig.json b/pubky-mcp-server/tsconfig.json new file mode 100644 index 0000000..156b6d5 --- /dev/null +++ b/pubky-mcp-server/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}