Skip to main content
Glama
utils.ts4.92 kB
/** * GitHub APIリクエストの作成とレスポンス処理用のユーティリティ関数。 */ import { createGitHubError } from './errors' /** * GitHub MCPサーバーのバージョン情報 */ export const VERSION = '0.1.0' // GitHubリクエストのオプション型 export type RequestOptions = { method?: string body?: unknown headers?: Record<string, string> } /** * コンテントタイプに基づいてレスポンスボディをパース */ async function parseResponseBody(response: Response): Promise<unknown> { const contentType = response.headers.get('content-type') if (contentType?.includes('application/json')) { return response.json() } return response.text() } /** * クエリパラメータ付きURLを生成 */ export function buildUrl( baseUrl: string, params: Record<string, string | number | undefined>, ): string { const url = new URL(baseUrl) Object.entries(params).forEach(([key, value]) => { if (value !== undefined) { url.searchParams.append(key, value.toString()) } }) return url.toString() } const USER_AGENT = `claude-ts-mcps/github/v${VERSION}` /** * 指定したアカウントプロファイルのGitHubトークンを取得する * @param accountProfile アカウントプロファイル名(省略時はデフォルト) * @returns 対応するGitHubトークン */ export function getGitHubToken(accountProfile?: string): string | undefined { if (!accountProfile || accountProfile === 'default') { // 従来の環境変数名をデフォルトとして維持 return process.env.GITHUB_PERSONAL_ACCESS_TOKEN } // アカウントプロファイル名に基づいてトークンを取得 // 例: 'work' → GITHUB_TOKEN_WORK const tokenEnvName = `GITHUB_TOKEN_${accountProfile.toUpperCase()}` return process.env[tokenEnvName] } /** * 適切なエラー処理を伴うGitHub APIリクエストを行う */ export async function githubRequest( url: string, options: RequestOptions = {}, accountProfile?: string, ): Promise<unknown> { const headers: Record<string, string> = { Accept: 'application/vnd.github.v3+json', 'Content-Type': 'application/json', 'User-Agent': USER_AGENT, ...options.headers, } const token = getGitHubToken(accountProfile) if (token) { headers['Authorization'] = `Bearer ${token}` } const response = await fetch(url, { method: options.method || 'GET', headers, body: options.body ? JSON.stringify(options.body) : undefined, }) const responseBody = await parseResponseBody(response) if (!response.ok) { throw createGitHubError(response.status, responseBody) } return responseBody } /** * GitHub操作用にブランチ名を検証 */ export function validateBranchName(branch: string): string { const sanitized = branch.trim() if (!sanitized) { throw new Error('Branch name cannot be empty') } if (sanitized.includes('..')) { throw new Error("Branch name cannot contain '..'") } if (/[\s~^:?*[\\\]]/.test(sanitized)) { throw new Error('Branch name contains invalid characters') } if (sanitized.startsWith('/') || sanitized.endsWith('/')) { throw new Error("Branch name cannot start or end with '/'") } if (sanitized.endsWith('.lock')) { throw new Error("Branch name cannot end with '.lock'") } return sanitized } /** * GitHub操作用にリポジトリ名を検証 */ export function validateRepositoryName(name: string): string { const sanitized = name.trim().toLowerCase() if (!sanitized) { throw new Error('Repository name cannot be empty') } if (!/^[a-z0-9_.-]+$/.test(sanitized)) { throw new Error( 'Repository name can only contain lowercase letters, numbers, hyphens, periods, and underscores', ) } if (sanitized.startsWith('.') || sanitized.endsWith('.')) { throw new Error('Repository name cannot start or end with a period') } return sanitized } /** * GitHub操作用にオーナー名を検証 */ export function validateOwnerName(owner: string): string { const sanitized = owner.trim().toLowerCase() if (!sanitized) { throw new Error('Owner name cannot be empty') } if (!/^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}$/.test(sanitized)) { throw new Error( 'Owner name must start with a letter or number and can contain up to 39 characters', ) } return sanitized } /** * リポジトリ内にブランチが存在するか確認 */ export async function checkBranchExists( owner: string, repo: string, branch: string, ): Promise<boolean> { try { await githubRequest( `https://api.github.com/repos/${owner}/${repo}/branches/${branch}`, ) return true } catch (error) { if ( error && typeof error === 'object' && 'status' in error && error.status === 404 ) { return false } throw error } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/drapon/claude-mcp-servers'

If you have feedback or need assistance with the MCP directory API, please join our Discord server