Skip to content

Example: GitHub Issues

A GitHub API wrapper as MCP tools. AI can list, create, comment, label, and close issues.

Full code

typescript
// src/index.ts
import {
  defineServer, defineTool,
  timeoutPlugin, retryPlugin, cachePlugin, authPlugin,
} from '@airmcp-dev/core';

const GITHUB_TOKEN = process.env.GITHUB_TOKEN!;
const DEFAULT_OWNER = process.env.GITHUB_OWNER || '';
const DEFAULT_REPO = process.env.GITHUB_REPO || '';

async function gh(path: string, options?: RequestInit): Promise<any> {
  const res = await fetch(`https://api.github.com${path}`, {
    ...options,
    headers: {
      'Authorization': `token ${GITHUB_TOKEN}`,
      'Accept': 'application/vnd.github.v3+json',
      'Content-Type': 'application/json',
      ...options?.headers,
    },
  });
  if (!res.ok) throw new Error(`GitHub API ${res.status}: ${await res.text()}`);
  return res.json();
}

function resolveRepo(owner?: string, repo?: string) {
  const o = owner || DEFAULT_OWNER;
  const r = repo || DEFAULT_REPO;
  if (!o || !r) throw new Error('Specify owner/repo or set GITHUB_OWNER/GITHUB_REPO env vars');
  return { owner: o, repo: r };
}

const server = defineServer({
  name: 'github-issues',
  version: '1.0.0',
  transport: { type: 'sse', port: 3510 },

  use: [
    authPlugin({ type: 'api-key', keys: [process.env.MCP_API_KEY!], publicTools: ['issue_list'] }),
    timeoutPlugin(15_000),
    retryPlugin({ maxRetries: 2, retryOn: (e) => e.message.includes('fetch failed') }),
    cachePlugin({ ttlMs: 30_000, exclude: ['issue_create', 'issue_comment', 'issue_label', 'issue_close'] }),
  ],

  tools: [
    defineTool('issue_list', {
      description: 'List issues',
      params: {
        owner: { type: 'string', optional: true }, repo: { type: 'string', optional: true },
        state: { type: 'string', description: '"open", "closed", "all"', optional: true },
        labels: { type: 'string', description: 'Comma-separated labels', optional: true },
        limit: { type: 'number', optional: true },
      },
      handler: async ({ owner, repo, state, labels, limit }) => {
        const { owner: o, repo: r } = resolveRepo(owner, repo);
        const params = new URLSearchParams({ state: state || 'open', per_page: String(limit || 10) });
        if (labels) params.set('labels', labels);
        const issues = await gh(`/repos/${o}/${r}/issues?${params}`);
        return issues.map((i: any) => ({
          number: i.number, title: i.title, state: i.state,
          labels: i.labels.map((l: any) => l.name), author: i.user.login, comments: i.comments,
        }));
      },
    }),

    defineTool('issue_read', {
      description: 'Read issue details and comments',
      params: { number: 'number', owner: { type: 'string', optional: true }, repo: { type: 'string', optional: true } },
      handler: async ({ number, owner, repo }) => {
        const { owner: o, repo: r } = resolveRepo(owner, repo);
        const [issue, comments] = await Promise.all([
          gh(`/repos/${o}/${r}/issues/${number}`),
          gh(`/repos/${o}/${r}/issues/${number}/comments`),
        ]);
        return {
          number: issue.number, title: issue.title, body: issue.body, state: issue.state,
          labels: issue.labels.map((l: any) => l.name),
          comments: comments.map((c: any) => ({ author: c.user.login, body: c.body, created: c.created_at })),
        };
      },
    }),

    defineTool('issue_create', {
      description: 'Create a new issue',
      params: {
        title: 'string',
        body: { type: 'string', optional: true },
        labels: { type: 'string', optional: true },
        owner: { type: 'string', optional: true }, repo: { type: 'string', optional: true },
      },
      handler: async ({ title, body, labels, owner, repo }) => {
        const { owner: o, repo: r } = resolveRepo(owner, repo);
        const payload: any = { title };
        if (body) payload.body = body;
        if (labels) payload.labels = labels.split(',').map((l: string) => l.trim());
        const issue = await gh(`/repos/${o}/${r}/issues`, { method: 'POST', body: JSON.stringify(payload) });
        return { number: issue.number, url: issue.html_url, message: `#${issue.number} created` };
      },
    }),

    defineTool('issue_comment', {
      description: 'Add a comment to an issue',
      params: { number: 'number', body: 'string', owner: { type: 'string', optional: true }, repo: { type: 'string', optional: true } },
      handler: async ({ number, body, owner, repo }) => {
        const { owner: o, repo: r } = resolveRepo(owner, repo);
        await gh(`/repos/${o}/${r}/issues/${number}/comments`, { method: 'POST', body: JSON.stringify({ body }) });
        return { message: `Comment added to #${number}` };
      },
    }),

    defineTool('issue_close', {
      description: 'Close an issue',
      params: { number: 'number', owner: { type: 'string', optional: true }, repo: { type: 'string', optional: true } },
      handler: async ({ number, owner, repo }) => {
        const { owner: o, repo: r } = resolveRepo(owner, repo);
        await gh(`/repos/${o}/${r}/issues/${number}`, { method: 'PATCH', body: JSON.stringify({ state: 'closed' }) });
        return { number, message: `#${number} closed` };
      },
    }),

    defineTool('issue_label', {
      description: 'Add labels to an issue',
      params: { number: 'number', labels: 'string', owner: { type: 'string', optional: true }, repo: { type: 'string', optional: true } },
      handler: async ({ number, labels, owner, repo }) => {
        const { owner: o, repo: r } = resolveRepo(owner, repo);
        const labelList = labels.split(',').map((l: string) => l.trim());
        await gh(`/repos/${o}/${r}/issues/${number}/labels`, { method: 'POST', body: JSON.stringify({ labels: labelList }) });
        return { number, labels: labelList, message: `Labels added to #${number}` };
      },
    }),
  ],
});

server.start();

Environment variables

bash
GITHUB_TOKEN=ghp_xxxxxxxxxxxx       # Required
GITHUB_OWNER=your-org               # Default owner
GITHUB_REPO=your-repo               # Default repo
MCP_API_KEY=your-key                # MCP auth (optional)

Usage

  • "List open issues" → issue_list
  • "Show issue #42 details and comments" → issue_read
  • "Create an issue titled 'Fix CI pipeline'" → issue_create
  • "Add a comment to #42: 'Confirmed, will fix next sprint'" → issue_comment
  • "Add bug, priority-high labels to #42" → issue_label
  • "Close issue #42" → issue_close

Released under the Apache-2.0 License.