github-assistant-mcp
# GitHub Assistant MCP
A small, self-contained [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that exposes five read-focused tools to an AI coding assistant (e.g. OpenCode). It lets the assistant inspect a local workspace and pull a public GitHub profile over a clean, sandboxed stdio transport.
> "A simple GitHub MCP server for OpenCode."
---
## Table of Contents
- [Overview](#overview)
- [How It Works (Architecture)](#how-it-works-architecture)
- [Transport & Lifecycle](#transport--lifecycle)
- [Tools Reference](#tools-reference)
- [Security Model](#security-model)
- [Project Walkthrough](#project-walkthrough)
- [Configuration](#configuration)
- [Building & Running](#building--running)
- [File Structure](#file-structure)
- [Limitations](#limitations)
---
## Overview
The server is a **local MCP server** started by OpenCode as a child process. It speaks the MCP protocol over **stdio** (`stdin`/`stdout`) and registers five tools. The assistant calls those tools; the server performs the work (filesystem reads, a `git diff`, or a GitHub API call) and returns structured text results.
Everything that touches the filesystem is confined to a single `WORKSPACE_ROOT` directory, so the assistant can never read or escape outside the project folder.
---
## How It Works (Architecture)
```
┌─────────────────────────┐ stdio (MCP/JSON-RPC) ┌──────────────────────────────┐
│ │ ───────────────────────────────▶ │ github-assistant (this) │
│ OpenCode / AI │ tool call: get_github_profile │ │
│ Assistant │ │ ┌────────────────────────┐ │
│ │ ◀─────────────────────────────── │ │ McpServer │ │
│ - sees 5 tools │ result (JSON text) │ │ (server.ts) │ │
│ - calls them │ │ └───────────┬────────────┘ │
│ - sandbox enforced │ │ │ registerTools │
└─────────────────────────┘ └──────────────┼──────────────┘
▼
┌────────────────────────────────┐
│ tools.ts (5 tool handlers) │
└───┬──────┬──────┬──────┬─────┬──┘
┌───────────────┘ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌─────────┐ ┌────────────┐
│ github.ts │ │ workspace.ts│ │ git.ts │ │ paths.ts │
│ GitHub API │ │ list/read/ │ │ git diff│ │ resolve │
│ (fetch) │ │ search │ │ │ │ sandbox │
└─────┬──────┘ └─────┬──────┘ └────┬────┘ └─────┬──────┘
│ │ │ │
▼ ▼ ▼ ▼
api.github.com WORKSPACE_ROOT/* git CLI config.ts
(files only) (cwd=root) WORKSPACE_ROOT
```
**Data flow for a single tool call:**
```
Assistant ──JSON-RPC request──▶ McpServer
│
▼
tool handler (tools.ts)
│ validates args with zod
▼
business logic (github / workspace / git / paths)
│ resolveWorkspacePath() enforces sandbox
▼
result helper (result.ts) → { content: [{ type:"text", text }] }
│
▼
Assistant ◀──JSON-RPC response── McpServer
```
---
## Transport & Lifecycle
- **Type:** `local` — OpenCode launches the server as a child process.
- **Transport:** `stdio` via `serveStdio()` from `@modelcontextprotocol/server/stdio`.
- **Startup sequence:**
1. `node dist/server.js` is executed (declared in `opencode.json`) with `cwd = "."`.
2. `createServer()` builds an `McpServer` named `github-assistant` (v1.0.0).
3. `registerTools(server)` wires up the five tools.
4. `serveStdio(createServer)` begins reading JSON-RPC messages from stdin and writing results to stdout.
- **Shutdown:** OpenCode terminates the process when the session ends.
Because the process inherits OpenCode's working directory, `WORKSPACE_ROOT` resolves to the project directory (`path.resolve(process.cwd())`).
---
## Tools Reference
All tools are registered in `src/tools.ts` and return MCP text results (JSON or plain text).
### 1. `get_github_profile`
Fetches the **public** GitHub profile of the hardcoded user (`imshashwatsingh`).
- **Inputs:** none
- **Backend:** `fetch()` to `https://api.github.com/users/imshashwatsingh` with `Accept: application/vnd.github+json` and a `User-Agent` header.
- **Returns:** username, name, company, location, bio, public repos/gists, followers, following, profile URL, created/updated timestamps.
- **File:** `src/github.ts`
### 2. `list_files`
Lists files under a workspace directory up to a depth.
- **Inputs:** `path` (default `"."`), `maxDepth` (0–10, default 3)
- **Backend:** recursive `collectFiles()` in `src/workspace.ts` — skips symlinks (no loops) and ignores configured directories (`node_modules`, `.git`, `dist`, `.next`, `coverage`, `.cache`). Capped at `MAX_RESULTS` (500).
- **Returns:** workspace root, file count, and relative file paths.
- **File:** `src/workspace.ts`
### 3. `read_file`
Reads a UTF-8 text file with optional line range.
- **Inputs:** `path` (required), `startLine` (optional), `endLine` (optional)
- **Backend:** `readWorkspaceFile()` — enforces sandbox, rejects non-files, refuses files larger than `MAX_FILE_SIZE` (1 MB), and refuses binary extensions. Returns numbered lines.
- **Returns:** file content with `line: text` prefixes.
- **File:** `src/workspace.ts`
### 4. `search_context`
Keyword search across the workspace with surrounding context.
- **Inputs:** `query` (required), `path` (default `"."`), `maxResults` (1–100, default 50), `contextLines` (0–10, default 2)
- **Backend:** `searchContext()` collects files, filters text-only and size-bounded files, then scans each line (case-insensitive) and captures `contextLines` above/below every match.
- **Returns:** query, search path, match count, and matches with file/line/context.
- **File:** `src/workspace.ts`
### 5. `summarize_diff`
Inspects the current Git diff and returns a structured summary.
- **Inputs:** `staged` (default `false`), `base` (optional git ref), `path` (optional file/dir), `maxDiffChars` (1000–200000, default 50000)
- **Backend:** `summarizeDiff()` runs `git diff --no-ext-diff --unified=3` (with `--cached` / base ref / path filters) from `WORKSPACE_ROOT`. Stats are parsed from the unified diff itself (no second `git` call). Diff is truncated if it exceeds `maxDiffChars`.
- **Returns:** files changed, insertions, deletions, per-file stats, and the raw diff — or `{ empty: true }` when there are no changes.
- **File:** `src/git.ts`
---
## Security Model
The server is intentionally **read-only and sandboxed**:
| Concern | Protection |
|---------|------------|
| Path traversal (`../../etc/passwd`) | `resolveWorkspacePath()` (`src/paths.ts`) resolves the path, computes its relation to `WORKSPACE_ROOT`, and **throws** if it escapes (`..` prefix or absolute). |
| Binary file reads | `isProbablyTextFile()` blocks non-text extensions (`png`, `exe`, `pdf`, …). |
| Oversized files | `read_file` / `search_context` refuse files above `MAX_FILE_SIZE` (1 MB). |
| Symlink loops | `collectFiles()` skips symbolic links entirely. |
| Directory blow-up | Listing/searching capped at `MAX_RESULTS` (500) and `maxDepth` 10. |
| Write / delete / exec | **None.** The server has no write, delete, or arbitrary shell-exec tools. The only spawned process is `git` with a fixed argument shape. |
| Network | Only one outbound call: the read-only GitHub public API for a fixed user. |
> The sandbox boundary lives entirely in `paths.ts`. Any new tool that touches the filesystem **must** route paths through `resolveWorkspacePath()`.
---
## Project Walkthrough
1. **Entry point — `src/server.ts`**
`createServer()` instantiates `McpServer` and calls `registerTools()`. `serveStdio()` bridges it to stdin/stdout.
2. **Tool registration — `src/tools.ts`**
Five `server.registerTool(...)` calls. Each declares a description, a zod-validated `inputSchema`, and an async handler. Handlers delegate to the modules below and wrap output with `result.ts` helpers.
3. **Configuration — `src/config.ts`**
Central constants: `WORKSPACE_ROOT` (resolved from `process.cwd()`), size/result limits, the GitHub username/URL, and ignore/binary sets.
4. **Path safety — `src/paths.ts`**
`resolveWorkspacePath()` is the sandbox gate. `toWorkspaceRelative()` turns absolute paths back into workspace-relative strings for display. `isProbablyTextFile()` classifies files by extension.
5. **Workspace I/O — `src/workspace.ts`**
`collectFiles()` (recursive listing), `readWorkspaceFile()` (safe read), and `searchContext()` (keyword scan). All go through `resolveWorkspacePath()`.
6. **GitHub — `src/github.ts`**
`fetchGitHubProfile()` calls the public API and maps the raw `GitHubUser` to the friendlier `GitHubProfile` shape.
7. **Git — `src/git.ts`**
`summarizeDiff()` builds and runs the `git diff` command; `parseDiffStats()` derives per-file insert/delete counts straight from the diff text.
8. **Results — `src/result.ts`**
Small helpers (`textResult`, `errorResult`, `errorWithContext`) standardize the MCP `content` envelope and error flagging.
---
## Configuration
`opencode.json` (project root) declares the server:
```json
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"github-assistant": {
"type": "local",
"command": ["node", "dist/server.js"],
"cwd": ".",
"enabled": true
}
}
}
```
Inside the server, behavior is tuned via constants in `src/config.ts`:
| Constant | Default | Meaning |
|----------|---------|---------|
| `WORKSPACE_ROOT` | `path.resolve(process.cwd())` | Sandbox root (project dir) |
| `MAX_FILE_SIZE` | `1 MB` | Max readable file size |
| `MAX_RESULTS` | `500` | Max files from list/search |
| `GITHUB_USERNAME` | `imshashwatsingh` | Profile target |
| `IGNORED_DIRECTORIES` | `node_modules`, `.git`, `dist`, … | Skipped while walking |
| `BINARY_EXTENSIONS` | `png`, `exe`, `pdf`, … | Treated as non-text |
---
## Building & Running
```bash
# install dependencies
npm install
# compile TypeScript -> dist/
npm run build
# start the server (used by opencode.json)
npm start
# run directly from source (no build step)
npm run dev
# the workspace must be a git repo for summarize_diff to work
git init
```
OpenCode picks up the server automatically from `opencode.json` once built (`dist/server.js`).
---
## File Structure
```
github_assistant_mcp/
├── opencode.json # MCP server declaration for OpenCode
├── package.json # scripts + dependencies
├── tsconfig.json # TypeScript config
├── src/
│ ├── server.ts # Entry point: create + serve McpServer
│ ├── tools.ts # Registers the 5 tools + handlers
│ ├── config.ts # Constants, limits, GitHub target
│ ├── paths.ts # Sandbox path resolution + helpers
│ ├── workspace.ts # list / read / search filesystem
│ ├── github.ts # GitHub profile fetch
│ ├── git.ts # git diff summary + stat parsing
│ └── result.ts # MCP result/error helpers
└── dist/ # Compiled output (npm run build)
```
---
## Limitations
- `get_github_profile` targets a **single hardcoded** user; it is not parameterized.
- `summarize_diff` reports working-tree changes only — **untracked** files are not shown by `git diff`.
- Filesystem tools are confined to `WORKSPACE_ROOT`; there is no cross-project access.
- All tools are **read-only** by design — no edits, deletions, or shell execution.
- No authentication: the GitHub call uses the unauthenticated public API (rate-limited to 60 req/hr per IP).
TDQS
Scored across 5 tools
The tools are clearly distinct: one fetches GitHub profile data, while the others handle local workspace file operations (listing, reading, searching, diffing). No functional overlap exists between them.
All tool names follow a consistent 'verb_noun' pattern (e.g., list_files, read_file, summarize_diff). The single compound name 'get_github_profile' still adheres to the same structure, maintaining a uniform convention.
Five tools is a reasonable number for a focused assistant, neither too sparse nor overwhelming. However, the mix leans heavily toward workspace operations rather than GitHub-specific actions, which slightly reduces appropriateness for the server's stated purpose.
The tool surface is severely incomplete for a GitHub assistant: it only covers profile retrieval and local file operations. Core GitHub workflows like issues, pull requests, repository management, and code search are entirely absent, making the toolset insufficient for its intended domain.