# Directory Structure ``` ├── .gitignore ├── index.ts ├── package.json ├── README.md └── wrangler.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | node_modules 2 | package-lock.json ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # UIThub MCP Server 2 | 3 | [](https://smithery.ai/server/@janwilmake/uithub-mcp) 4 | 5 | Model Context Protocol (MCP) server for interacting with the [uithub API](https://uithub.com), which provides a convenient way to fetch GitHub repository contents. 6 | 7 | This MCP server allows Claude to retrieve and analyze code from GitHub repositories, making it a powerful tool for understanding and discussing code. 8 | 9 | ## TODO 10 | 11 | - ✅ Simple MCP Server for Claude Desktop 12 | - Make MCP for cursor too https://docs.cursor.com/context/model-context-protocol 13 | - MCP cline support https://github.com/cline/mcp-marketplace 14 | - Button to learn to install MCPs on separate page. 15 | - Add patch api to MCP Server 16 | 17 | ## Features 18 | 19 | - Retrieve repository contents with smart filtering options 20 | - Specify file extensions to include or exclude 21 | - Integrate with Claude Desktop for natural language exploration of repositories 22 | 23 | ## Installation 24 | 25 | ### Installing via Smithery 26 | 27 | To install uithub-mcp for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@janwilmake/uithub-mcp): 28 | 29 | ```bash 30 | npx -y @smithery/cli install @janwilmake/uithub-mcp --client claude 31 | ``` 32 | 33 | ### Manual Installation 34 | 1. `npx uithub-mcp init` 35 | 2. restart claude 36 | ``` -------------------------------------------------------------------------------- /wrangler.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "$schema": "https://unpkg.com/wrangler@latest/config-schema.json", 3 | "name": "uithub-remote-mcp", 4 | "main": "index.ts", 5 | "compatibility_date": "2025-09-01", 6 | "route": { "custom_domain": true, "pattern": "mcp.uithub.com" } 7 | } 8 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "uithub-mcp", 3 | "version": "0.2.0", 4 | "description": "MCP server for interacting with UIThub API", 5 | "license": "MIT", 6 | "type": "module", 7 | "main": "index.ts", 8 | "files": [ 9 | "index.ts", 10 | "README.md" 11 | ], 12 | "dependencies": { 13 | "simplerauth-client": "0.0.19" 14 | } 15 | } 16 | ``` -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { withSimplerAuth } from "simplerauth-client"; 2 | 3 | interface Env { 4 | // Add any environment variables here if needed 5 | } 6 | 7 | interface AuthenticatedContext extends ExecutionContext { 8 | user: { 9 | id: string; 10 | name: string; 11 | username: string; 12 | profile_image_url?: string; 13 | verified?: boolean; 14 | }; 15 | accessToken: string; 16 | authenticated: boolean; 17 | } 18 | 19 | const TOOLS = [ 20 | { 21 | name: "getRepositoryContents", 22 | description: 23 | "Get repository contents from GitHub. Unless otherwise instructed, ensure to always first get the tree only (omitFiles:true) to get an idea of the file structure. Afterwards, use the different filters to get only the context relevant to cater to the user request.", 24 | inputSchema: { 25 | type: "object", 26 | properties: { 27 | owner: { 28 | type: "string", 29 | description: "GitHub repository owner", 30 | }, 31 | repo: { 32 | type: "string", 33 | description: "GitHub repository name", 34 | }, 35 | branch: { 36 | type: "string", 37 | description: "Branch name (defaults to main if not provided)", 38 | }, 39 | path: { 40 | type: "string", 41 | description: "File or directory path within the repository", 42 | }, 43 | ext: { 44 | type: "string", 45 | description: "Comma-separated list of file extensions to include", 46 | }, 47 | dir: { 48 | type: "string", 49 | description: "Comma-separated list of directories to include", 50 | }, 51 | excludeExt: { 52 | type: "string", 53 | description: "Comma-separated list of file extensions to exclude", 54 | }, 55 | excludeDir: { 56 | type: "string", 57 | description: "Comma-separated list of directories to exclude", 58 | }, 59 | maxFileSize: { 60 | type: "integer", 61 | description: "Maximum file size to include (in bytes)", 62 | }, 63 | maxTokens: { 64 | type: "integer", 65 | description: 66 | "Limit the response to a maximum number of tokens (defaults to 50000)", 67 | }, 68 | omitFiles: { 69 | type: "boolean", 70 | description: "If true, response will not include the file contents", 71 | }, 72 | omitTree: { 73 | type: "boolean", 74 | description: "If true, response will not include the directory tree", 75 | }, 76 | }, 77 | required: ["owner", "repo"], 78 | }, 79 | }, 80 | ]; 81 | 82 | async function handleMCPRequest( 83 | request: Request, 84 | env: Env, 85 | ctx: AuthenticatedContext 86 | ): Promise<Response> { 87 | // Handle CORS preflight 88 | if (request.method === "OPTIONS") { 89 | return new Response(null, { 90 | status: 204, 91 | headers: { 92 | "Access-Control-Allow-Origin": "*", 93 | "Access-Control-Allow-Methods": "POST, OPTIONS", 94 | "Access-Control-Allow-Headers": "Content-Type, Authorization, Accept", 95 | "Access-Control-Max-Age": "86400", 96 | }, 97 | }); 98 | } 99 | 100 | // Require authentication 101 | if (!ctx.authenticated) { 102 | return new Response( 103 | JSON.stringify({ 104 | jsonrpc: "2.0", 105 | id: null, 106 | error: { 107 | code: -32001, 108 | message: "Authentication required. Please login with GitHub first.", 109 | }, 110 | }), 111 | { 112 | status: 401, 113 | headers: { 114 | "Content-Type": "application/json", 115 | "Access-Control-Allow-Origin": "*", 116 | }, 117 | } 118 | ); 119 | } 120 | 121 | let message: any; 122 | try { 123 | message = await request.json(); 124 | } catch (error) { 125 | return createError(null, -32700, "Parse error"); 126 | } 127 | 128 | // Handle initialize 129 | if (message.method === "initialize") { 130 | return new Response( 131 | JSON.stringify({ 132 | jsonrpc: "2.0", 133 | id: message.id, 134 | result: { 135 | protocolVersion: "2025-03-26", 136 | capabilities: { tools: {} }, 137 | serverInfo: { 138 | name: "UIThub-Remote-MCP", 139 | version: "1.0.0", 140 | }, 141 | }, 142 | }), 143 | { 144 | headers: { 145 | "Content-Type": "application/json", 146 | "Access-Control-Allow-Origin": "*", 147 | }, 148 | } 149 | ); 150 | } 151 | 152 | // Handle initialized notification 153 | if (message.method === "notifications/initialized") { 154 | return new Response(null, { 155 | status: 202, 156 | headers: { 157 | "Access-Control-Allow-Origin": "*", 158 | }, 159 | }); 160 | } 161 | 162 | // Handle tools/list 163 | if (message.method === "tools/list") { 164 | return new Response( 165 | JSON.stringify({ 166 | jsonrpc: "2.0", 167 | id: message.id, 168 | result: { tools: TOOLS }, 169 | }), 170 | { 171 | headers: { 172 | "Content-Type": "application/json", 173 | "Access-Control-Allow-Origin": "*", 174 | }, 175 | } 176 | ); 177 | } 178 | 179 | // Handle tools/call 180 | if (message.method === "tools/call") { 181 | const { name, arguments: args } = message.params; 182 | 183 | if (name !== "getRepositoryContents") { 184 | return createError(message.id, -32602, `Unknown tool: ${name}`); 185 | } 186 | 187 | return await handleGetRepositoryContents(message.id, args, ctx.accessToken); 188 | } 189 | 190 | return createError(message.id, -32601, `Method not found: ${message.method}`); 191 | } 192 | 193 | async function handleGetRepositoryContents( 194 | messageId: any, 195 | args: any, 196 | accessToken: string 197 | ): Promise<Response> { 198 | const { 199 | owner, 200 | repo, 201 | branch = "main", 202 | path = "", 203 | ext, 204 | dir, 205 | excludeExt, 206 | excludeDir, 207 | maxFileSize, 208 | maxTokens = 50000, 209 | omitFiles, 210 | omitTree, 211 | } = args; 212 | 213 | if (!owner || !repo) { 214 | return createError( 215 | messageId, 216 | -32602, 217 | "Missing required parameters: owner and repo" 218 | ); 219 | } 220 | 221 | try { 222 | // Build URLSearchParams for UIThub API 223 | const params = new URLSearchParams(); 224 | if (ext) params.append("ext", ext); 225 | if (dir) params.append("dir", dir); 226 | if (excludeExt) params.append("exclude-ext", excludeExt); 227 | if (excludeDir) params.append("exclude-dir", excludeDir); 228 | if (maxFileSize) params.append("maxFileSize", maxFileSize.toString()); 229 | params.append("maxTokens", maxTokens.toString()); 230 | if (omitFiles) params.append("omitFiles", "true"); 231 | if (omitTree) params.append("omitTree", "true"); 232 | 233 | // Use the GitHub access token for UIThub API 234 | if (accessToken) { 235 | params.append("apiKey", accessToken); 236 | } 237 | 238 | // Construct the UIThub API URL 239 | const pathSegment = path ? `/${path}` : ""; 240 | const url = `https://uithub.com/${owner}/${repo}/tree/${branch}${pathSegment}?${params.toString()}`; 241 | 242 | // Make request to UIThub API 243 | const response = await fetch(url, { 244 | headers: { Accept: "text/markdown" }, 245 | }); 246 | 247 | if (!response.ok) { 248 | const error = await response.text(); 249 | return createError(messageId, -32603, `UIThub API error: ${error}`); 250 | } 251 | 252 | const responseText = await response.text(); 253 | 254 | return new Response( 255 | JSON.stringify({ 256 | jsonrpc: "2.0", 257 | id: messageId, 258 | result: { 259 | content: [{ type: "text", text: responseText }], 260 | isError: false, 261 | }, 262 | }), 263 | { 264 | headers: { 265 | "Content-Type": "application/json", 266 | "Access-Control-Allow-Origin": "*", 267 | }, 268 | } 269 | ); 270 | } catch (error) { 271 | return createError( 272 | messageId, 273 | -32603, 274 | `Error fetching repository contents: ${error.message}` 275 | ); 276 | } 277 | } 278 | 279 | function createError(id: any, code: number, message: string): Response { 280 | return new Response( 281 | JSON.stringify({ 282 | jsonrpc: "2.0", 283 | id, 284 | error: { code, message }, 285 | }), 286 | { 287 | status: 200, // JSON-RPC errors use 200 status 288 | headers: { 289 | "Content-Type": "application/json", 290 | "Access-Control-Allow-Origin": "*", 291 | }, 292 | } 293 | ); 294 | } 295 | 296 | // Main handler wrapped with SimplerAuth 297 | async function handler( 298 | request: Request, 299 | env: Env, 300 | ctx: AuthenticatedContext 301 | ): Promise<Response> { 302 | const url = new URL(request.url); 303 | 304 | // Handle MCP endpoint 305 | if (url.pathname === "/mcp") { 306 | return handleMCPRequest(request, env, ctx); 307 | } 308 | 309 | // Handle root - show connection instructions 310 | if (url.pathname === "/") { 311 | const loginUrl = ctx.authenticated 312 | ? "" 313 | : `<p><a href="/authorize">Login with GitHub</a> first to use the MCP server.</p>`; 314 | 315 | console.log({ user: ctx.user }); 316 | return new Response( 317 | `<!DOCTYPE html> 318 | <html> 319 | <head> 320 | <title>UIThub Remote MCP Server</title> 321 | <style> 322 | body { font-family: system-ui, sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; } 323 | pre { background: #f5f5f5; padding: 1rem; border-radius: 4px; overflow-x: auto; } 324 | .user-info { background: #e8f5e9; padding: 1rem; border-radius: 4px; margin: 1rem 0; } 325 | </style> 326 | </head> 327 | <body> 328 | <h1>UIThub Remote MCP Server</h1> 329 | 330 | ${ 331 | ctx.authenticated 332 | ? ` 333 | <div class="user-info"> 334 | <strong>Logged in as:</strong> ${ctx.user.name || ""} @${ 335 | ctx.user.login || ctx.user.username 336 | } 337 | </div> 338 | ` 339 | : loginUrl 340 | } 341 | 342 | <p>This is a remote MCP server that provides access to GitHub repositories through UIThub API.</p> 343 | 344 | <h2>Usage</h2> 345 | <p>Connect your MCP client to:</p> 346 | <pre>${url.origin}/mcp</pre> 347 | 348 | <p>Available tools:</p> 349 | <ul> 350 | <li><strong>getRepositoryContents</strong> - Get repository contents from GitHub with filtering options</li> 351 | </ul> 352 | 353 | <h2>Authentication</h2> 354 | <p>This server requires GitHub authentication. Your GitHub access token will be used to make requests to the UIThub API, allowing access to private repositories if you have permission.</p> 355 | 356 | ${ctx.authenticated ? `<p><a href="/logout">Logout</a></p>` : ""} 357 | </body> 358 | </html>`, 359 | { headers: { "Content-Type": "text/html" } } 360 | ); 361 | } 362 | 363 | // Handle 404 364 | return new Response("Not Found", { status: 404 }); 365 | } 366 | 367 | // Export the handler wrapped with SimplerAuth 368 | export default { 369 | fetch: withSimplerAuth(handler, { 370 | isLoginRequired: false, // We handle auth manually for MCP endpoint 371 | oauthProviderHost: "gh.simplerauth.com", 372 | scope: "repo read:user", 373 | }), 374 | }; 375 | ```