Move source files to src/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
20
src/inspect.ts
Normal file
20
src/inspect.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'dotenv/config';
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const page = await browser.newPage();
|
||||
await page.goto(`${process.env.GITEA_URL}/user/login`);
|
||||
|
||||
const elements = await page.evaluate(() =>
|
||||
[...document.querySelectorAll<HTMLInputElement | HTMLButtonElement>('input, button')].map(el => ({
|
||||
tag: el.tagName,
|
||||
type: (el as HTMLInputElement).type,
|
||||
name: (el as HTMLInputElement).name,
|
||||
id: el.id,
|
||||
class: el.className.slice(0, 60),
|
||||
value: (el as HTMLInputElement).value?.slice(0, 40),
|
||||
}))
|
||||
);
|
||||
|
||||
console.log(JSON.stringify(elements, null, 2));
|
||||
await browser.close();
|
||||
48
src/lib/auth.ts
Normal file
48
src/lib/auth.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { chromium, type Browser, type Page } from 'playwright';
|
||||
import 'dotenv/config';
|
||||
|
||||
function requireEnv(key: string): string {
|
||||
const value = process.env[key];
|
||||
if (!value) throw new Error(`Missing required env var: ${key}`);
|
||||
return value;
|
||||
}
|
||||
|
||||
export const GITEA_URL = requireEnv('GITEA_URL');
|
||||
export const USERNAME = requireEnv('GITEA_USERNAME');
|
||||
const PASSWORD = requireEnv('GITEA_PASSWORD');
|
||||
|
||||
export interface Session {
|
||||
browser: Browser;
|
||||
page: Page;
|
||||
}
|
||||
|
||||
export async function createSession(): Promise<Session> {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const page = await browser.newPage();
|
||||
|
||||
await page.goto(`${GITEA_URL}/user/login`, { waitUntil: 'load' });
|
||||
await page.fill('input[name="user_name"]', USERNAME);
|
||||
await page.fill('input[name="password"]', PASSWORD);
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'load' }),
|
||||
page.getByRole('button', { name: /sign in/i }).click(),
|
||||
]);
|
||||
|
||||
if (page.url().includes('change_password')) {
|
||||
await page.fill('input[name="password"]', PASSWORD);
|
||||
await page.fill('input[name="retype"]', PASSWORD);
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'load' }),
|
||||
page.getByRole('button', { name: /update password/i }).click(),
|
||||
]);
|
||||
}
|
||||
|
||||
const loggedIn = (await page.$('.avatar')) !== null;
|
||||
if (!loggedIn) {
|
||||
await browser.close();
|
||||
throw new Error('Login failed');
|
||||
}
|
||||
|
||||
return { browser, page };
|
||||
}
|
||||
280
src/lib/gitea.ts
Normal file
280
src/lib/gitea.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
import { GITEA_URL, USERNAME, type Session } from './auth';
|
||||
|
||||
export interface Repo {
|
||||
name: string;
|
||||
url: string;
|
||||
repo: string;
|
||||
}
|
||||
|
||||
export interface RepoFile {
|
||||
type: 'file' | 'dir';
|
||||
name: string;
|
||||
message: string | undefined;
|
||||
}
|
||||
|
||||
export interface RepoView {
|
||||
branch: string;
|
||||
files: RepoFile[];
|
||||
readme: string;
|
||||
cloneUrl: string;
|
||||
}
|
||||
|
||||
export interface PR {
|
||||
number: string | undefined;
|
||||
title: string | undefined;
|
||||
url: string | undefined;
|
||||
labels: string[];
|
||||
}
|
||||
|
||||
export interface PRDetails {
|
||||
title: string | undefined;
|
||||
state: string | undefined;
|
||||
body: string | undefined;
|
||||
headBranch: string | undefined;
|
||||
baseBranch: string | undefined;
|
||||
comments: Array<{ author: string | undefined; body: string | undefined }>;
|
||||
canMerge: boolean;
|
||||
isClosed: boolean;
|
||||
isMerged: boolean;
|
||||
}
|
||||
|
||||
// Create a new repository
|
||||
export async function createRepo(
|
||||
{ page }: Session,
|
||||
name: string,
|
||||
description = '',
|
||||
isPrivate = false,
|
||||
): Promise<{ url: string; cloneUrl: string }> {
|
||||
await page.goto(`${GITEA_URL}/repo/create`, { waitUntil: 'load' });
|
||||
|
||||
await page.fill('input[name="repo_name"]', name);
|
||||
if (description) await page.fill('textarea[name="description"]', description);
|
||||
if (isPrivate) await page.check('input[name="private"]');
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'load' }),
|
||||
page.getByRole('button', { name: 'Create Repository' }).click(),
|
||||
]);
|
||||
|
||||
const cloneUrl =
|
||||
(await page.locator('#repo-clone-https, input#clone-url').inputValue().catch(() => '')) ||
|
||||
`${GITEA_URL}/${USERNAME}/${name}.git`;
|
||||
|
||||
return { url: page.url(), cloneUrl };
|
||||
}
|
||||
|
||||
// List repos visible to the logged-in user
|
||||
export async function listRepos({ page }: Session): Promise<Repo[]> {
|
||||
await page.goto(`${GITEA_URL}/${USERNAME}`, { waitUntil: 'load' });
|
||||
|
||||
const reposTab = page.locator('a[href*="?tab=repos"]');
|
||||
if (await reposTab.count() > 0) {
|
||||
await reposTab.click();
|
||||
await page.waitForLoadState('load');
|
||||
}
|
||||
|
||||
return page.evaluate((username: string) => {
|
||||
const seen = new Set<string>();
|
||||
return [...document.querySelectorAll<HTMLAnchorElement>(`a[href^="/${username}/"]`)]
|
||||
.filter(a => {
|
||||
const parts = (a.getAttribute('href') ?? '').split('/').filter(Boolean);
|
||||
return parts.length === 2;
|
||||
})
|
||||
.map(a => ({
|
||||
name: a.textContent?.trim() ?? '',
|
||||
url: a.href,
|
||||
repo: (a.getAttribute('href') ?? '').split('/')[2] ?? '',
|
||||
}))
|
||||
.filter(r => r.name && !seen.has(r.url) && !!seen.add(r.url));
|
||||
}, USERNAME);
|
||||
}
|
||||
|
||||
// View a repo's root: file list, default branch, README excerpt
|
||||
export async function viewRepo({ page }: Session, owner: string, repo: string): Promise<RepoView> {
|
||||
await page.goto(`${GITEA_URL}/${owner}/${repo}`, { waitUntil: 'load' });
|
||||
|
||||
return page.evaluate((): RepoView => {
|
||||
const files = [...document.querySelectorAll('table.files tr, .repository.file.list table tr')]
|
||||
.slice(1)
|
||||
.map(row => {
|
||||
const icon = row.querySelector('svg, .octicon');
|
||||
const type: 'file' | 'dir' =
|
||||
icon?.classList.contains('octicon-file-directory') ||
|
||||
icon?.getAttribute('aria-label')?.includes('dir')
|
||||
? 'dir' : 'file';
|
||||
const nameEl = row.querySelector('td.name a, td a.muted');
|
||||
const msgEl = row.querySelector('td.message a');
|
||||
return nameEl
|
||||
? { type, name: nameEl.textContent?.trim() ?? '', message: msgEl?.textContent?.trim() }
|
||||
: null;
|
||||
})
|
||||
.filter((f): f is RepoFile => f !== null);
|
||||
|
||||
const branch =
|
||||
document.querySelector('.branch-dropdown-button, [data-clipboard-text]')?.textContent?.trim() ||
|
||||
(document.querySelector('input[name="ref"]') as HTMLInputElement | null)?.value ||
|
||||
'';
|
||||
|
||||
const readme =
|
||||
document.querySelector('#readme .markdown-body, #readme article, .plain-text pre')
|
||||
?.textContent?.slice(0, 3000) ?? '';
|
||||
|
||||
const cloneUrl =
|
||||
(document.querySelector('#repo-clone-https, input#clone-url') as HTMLInputElement | null)?.value ?? '';
|
||||
|
||||
return { branch, files, readme, cloneUrl };
|
||||
});
|
||||
}
|
||||
|
||||
// Get raw file content
|
||||
export async function viewFile(
|
||||
{ page }: Session,
|
||||
owner: string,
|
||||
repo: string,
|
||||
branch: string,
|
||||
filepath: string,
|
||||
): Promise<string> {
|
||||
const url = `${GITEA_URL}/${owner}/${repo}/raw/branch/${encodeURIComponent(branch)}/${filepath}`;
|
||||
const response = await page.goto(url, { waitUntil: 'load' });
|
||||
if (!response) throw new Error('No response received');
|
||||
return response.text();
|
||||
}
|
||||
|
||||
// List pull requests for a repo
|
||||
export async function listPRs(
|
||||
{ page }: Session,
|
||||
owner: string,
|
||||
repo: string,
|
||||
state: 'open' | 'closed' | 'all' = 'open',
|
||||
): Promise<PR[]> {
|
||||
await page.goto(`${GITEA_URL}/${owner}/${repo}/pulls?state=${state}&type=pullrequests`, { waitUntil: 'load' });
|
||||
|
||||
return page.evaluate((): PR[] => {
|
||||
return [...document.querySelectorAll('.issue.list .item')]
|
||||
.map(el => {
|
||||
const titleEl = el.querySelector<HTMLAnchorElement>('.title, a.title');
|
||||
const numberEl = el.querySelector('.index');
|
||||
const labels = [...el.querySelectorAll('.label')].map(l => l.textContent?.trim() ?? '');
|
||||
return {
|
||||
number: numberEl?.textContent?.replace('#', '').trim(),
|
||||
title: titleEl?.textContent?.trim(),
|
||||
url: titleEl?.href ?? el.querySelector('a')?.href,
|
||||
labels,
|
||||
};
|
||||
})
|
||||
.filter(pr => pr.title);
|
||||
});
|
||||
}
|
||||
|
||||
// Create a new pull request
|
||||
export async function createPR(
|
||||
{ page }: Session,
|
||||
owner: string,
|
||||
repo: string,
|
||||
head: string,
|
||||
base: string,
|
||||
title: string,
|
||||
body = '',
|
||||
): Promise<{ url: string; number: string | undefined }> {
|
||||
await page.goto(`${GITEA_URL}/${owner}/${repo}/compare/${base}...${head}`, { waitUntil: 'load' });
|
||||
|
||||
await page.fill('input[name="title"]', title);
|
||||
if (body) {
|
||||
await page.locator('textarea[name="content"], #content').fill(body);
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'load' }),
|
||||
page.locator('button[type="submit"]:has-text("Create"), input[type="submit"]').first().click(),
|
||||
]);
|
||||
|
||||
return {
|
||||
url: page.url(),
|
||||
number: page.url().match(/\/pulls\/(\d+)/)?.[1],
|
||||
};
|
||||
}
|
||||
|
||||
// View a specific pull request: details + comments
|
||||
export async function viewPR(
|
||||
{ page }: Session,
|
||||
owner: string,
|
||||
repo: string,
|
||||
number: number,
|
||||
): Promise<PRDetails> {
|
||||
await page.goto(`${GITEA_URL}/${owner}/${repo}/pulls/${number}`, { waitUntil: 'load' });
|
||||
|
||||
return page.evaluate((): PRDetails => {
|
||||
const comments = [...document.querySelectorAll('.comment:not(.first)')]
|
||||
.map(c => ({
|
||||
author: c.querySelector('.author')?.textContent?.trim(),
|
||||
body: c.querySelector('.render-content')?.textContent?.trim(),
|
||||
}))
|
||||
.filter(c => c.body);
|
||||
|
||||
return {
|
||||
title: document.querySelector('.issue-title, h1.issue-title')?.textContent?.trim(),
|
||||
state: document.querySelector('.issue-state-label, .label.green, .label.red, .label.purple')?.textContent?.trim(),
|
||||
body: document.querySelector('.comment.first .render-content, .post-content')?.textContent?.trim(),
|
||||
headBranch: document.querySelector('.head-branch, .compare-branch')?.textContent?.trim(),
|
||||
baseBranch: document.querySelector('.base-branch')?.textContent?.trim(),
|
||||
comments,
|
||||
canMerge: !!document.querySelector('button.merge-button, .merge-section button[type="submit"]'),
|
||||
isClosed: !!document.querySelector('.issue-state-label')?.textContent?.toLowerCase().includes('closed'),
|
||||
isMerged: !!document.querySelector('.merged-section, .label.purple'),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Comment on a pull request
|
||||
export async function commentPR(
|
||||
{ page }: Session,
|
||||
owner: string,
|
||||
repo: string,
|
||||
number: number,
|
||||
body: string,
|
||||
): Promise<{ url: string }> {
|
||||
await page.goto(`${GITEA_URL}/${owner}/${repo}/pulls/${number}`, { waitUntil: 'load' });
|
||||
|
||||
await page.locator('textarea[name="content"]').last().fill(body);
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'load' }),
|
||||
page.locator('button[type="submit"]:has-text("Comment")').last().click(),
|
||||
]);
|
||||
|
||||
return { url: page.url() };
|
||||
}
|
||||
|
||||
// Merge a pull request
|
||||
export async function mergePR(
|
||||
{ page }: Session,
|
||||
owner: string,
|
||||
repo: string,
|
||||
number: number,
|
||||
): Promise<{ url: string; merged: true }> {
|
||||
await page.goto(`${GITEA_URL}/${owner}/${repo}/pulls/${number}`, { waitUntil: 'load' });
|
||||
|
||||
const mergeBtn = page.locator('button.merge-button, .merge-section button[type="submit"]').first();
|
||||
await mergeBtn.waitFor({ state: 'visible' });
|
||||
await mergeBtn.click();
|
||||
await page.waitForLoadState('load');
|
||||
|
||||
return { url: page.url(), merged: true };
|
||||
}
|
||||
|
||||
// Close a pull request without merging
|
||||
export async function closePR(
|
||||
{ page }: Session,
|
||||
owner: string,
|
||||
repo: string,
|
||||
number: number,
|
||||
): Promise<{ url: string; closed: true }> {
|
||||
await page.goto(`${GITEA_URL}/${owner}/${repo}/pulls/${number}`, { waitUntil: 'load' });
|
||||
|
||||
const closeBtn = page.locator('button:has-text("Close"), a:has-text("Close Pull Request")').first();
|
||||
await closeBtn.waitFor({ state: 'visible' });
|
||||
await closeBtn.click();
|
||||
await page.waitForLoadState('load');
|
||||
|
||||
return { url: page.url(), closed: true };
|
||||
}
|
||||
44
src/login.ts
Normal file
44
src/login.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'dotenv/config';
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const GITEA_URL = process.env.GITEA_URL!;
|
||||
const USERNAME = process.env.GITEA_USERNAME!;
|
||||
const PASSWORD = process.env.GITEA_PASSWORD!;
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const page = await browser.newPage();
|
||||
|
||||
console.log('Navigating to login page...');
|
||||
await page.goto(`${GITEA_URL}/user/login`, { waitUntil: 'load' });
|
||||
console.log('URL:', page.url());
|
||||
|
||||
await page.fill('input[name="user_name"]', USERNAME);
|
||||
await page.fill('input[name="password"]', PASSWORD);
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'load' }),
|
||||
page.getByRole('button', { name: /sign in/i }).click(),
|
||||
]);
|
||||
|
||||
console.log('After login URL:', page.url());
|
||||
|
||||
if (page.url().includes('change_password')) {
|
||||
console.log('Password change required, reusing same password...');
|
||||
await page.fill('input[name="password"]', PASSWORD);
|
||||
await page.fill('input[name="retype"]', PASSWORD);
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'load' }),
|
||||
page.getByRole('button', { name: /update password/i }).click(),
|
||||
]);
|
||||
console.log('After password change URL:', page.url());
|
||||
}
|
||||
|
||||
console.log('Page title:', await page.title());
|
||||
|
||||
const loggedIn = (await page.$('.avatar')) !== null;
|
||||
console.log(loggedIn ? `SUCCESS: Logged in as ${USERNAME}` : 'Could not confirm login, dumping page text:');
|
||||
if (!loggedIn) {
|
||||
console.log((await page.innerText('body')).slice(0, 1000));
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
206
src/server.ts
Normal file
206
src/server.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { createSession, type Session } from './lib/auth';
|
||||
import * as gitea from './lib/gitea';
|
||||
|
||||
const server = new Server(
|
||||
{ name: 'gitea-browser', version: '1.0.0' },
|
||||
{ capabilities: { tools: {} } }
|
||||
);
|
||||
|
||||
async function withSession<T>(fn: (session: Session) => Promise<T>): Promise<T> {
|
||||
const session = await createSession();
|
||||
try {
|
||||
return await fn(session);
|
||||
} finally {
|
||||
await session.browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
const TOOLS = [
|
||||
{
|
||||
name: 'create_repo',
|
||||
description: 'Create a new repository on Gitea.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
required: ['name'],
|
||||
properties: {
|
||||
name: { type: 'string', description: 'Repository name' },
|
||||
description: { type: 'string', description: 'Optional description' },
|
||||
private: { type: 'boolean', description: 'Make the repo private (default: false)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'list_repos',
|
||||
description: 'List repositories accessible to the Gitea agent account.',
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
{
|
||||
name: 'view_repo',
|
||||
description: 'View a Gitea repository: file list, default branch, and README excerpt.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
required: ['owner', 'repo'],
|
||||
properties: {
|
||||
owner: { type: 'string', description: 'Repository owner (username or org)' },
|
||||
repo: { type: 'string', description: 'Repository name' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'view_file',
|
||||
description: 'Get the raw content of a file in a Gitea repository.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
required: ['owner', 'repo', 'branch', 'filepath'],
|
||||
properties: {
|
||||
owner: { type: 'string' },
|
||||
repo: { type: 'string' },
|
||||
branch: { type: 'string', description: 'Branch name' },
|
||||
filepath: { type: 'string', description: 'Path to the file (e.g. src/main.js)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'list_prs',
|
||||
description: 'List pull requests for a Gitea repository.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
required: ['owner', 'repo'],
|
||||
properties: {
|
||||
owner: { type: 'string' },
|
||||
repo: { type: 'string' },
|
||||
state: { type: 'string', enum: ['open', 'closed', 'all'], default: 'open' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'create_pr',
|
||||
description: 'Create a new pull request in a Gitea repository.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
required: ['owner', 'repo', 'head', 'base', 'title'],
|
||||
properties: {
|
||||
owner: { type: 'string' },
|
||||
repo: { type: 'string' },
|
||||
head: { type: 'string', description: 'Source branch (the branch with changes)' },
|
||||
base: { type: 'string', description: 'Target branch to merge into' },
|
||||
title: { type: 'string' },
|
||||
body: { type: 'string', description: 'PR description (optional)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'view_pr',
|
||||
description: 'View details of a pull request including title, status, branches, and comments.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
required: ['owner', 'repo', 'number'],
|
||||
properties: {
|
||||
owner: { type: 'string' },
|
||||
repo: { type: 'string' },
|
||||
number: { type: 'number', description: 'PR number' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'comment_pr',
|
||||
description: 'Add a comment to a pull request.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
required: ['owner', 'repo', 'number', 'body'],
|
||||
properties: {
|
||||
owner: { type: 'string' },
|
||||
repo: { type: 'string' },
|
||||
number: { type: 'number' },
|
||||
body: { type: 'string', description: 'Comment text' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'merge_pr',
|
||||
description: 'Merge an open pull request.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
required: ['owner', 'repo', 'number'],
|
||||
properties: {
|
||||
owner: { type: 'string' },
|
||||
repo: { type: 'string' },
|
||||
number: { type: 'number' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'close_pr',
|
||||
description: 'Close a pull request without merging.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
required: ['owner', 'repo', 'number'],
|
||||
properties: {
|
||||
owner: { type: 'string' },
|
||||
repo: { type: 'string' },
|
||||
number: { type: 'number' },
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args = {} } = request.params;
|
||||
const a = args as Record<string, unknown>;
|
||||
|
||||
try {
|
||||
let result: unknown;
|
||||
|
||||
switch (name) {
|
||||
case 'create_repo':
|
||||
result = await withSession(s => gitea.createRepo(s, a.name as string, a.description as string | undefined, a.private as boolean | undefined));
|
||||
break;
|
||||
case 'list_repos':
|
||||
result = await withSession(s => gitea.listRepos(s));
|
||||
break;
|
||||
case 'view_repo':
|
||||
result = await withSession(s => gitea.viewRepo(s, a.owner as string, a.repo as string));
|
||||
break;
|
||||
case 'view_file':
|
||||
result = await withSession(s => gitea.viewFile(s, a.owner as string, a.repo as string, a.branch as string, a.filepath as string));
|
||||
break;
|
||||
case 'list_prs':
|
||||
result = await withSession(s => gitea.listPRs(s, a.owner as string, a.repo as string, a.state as 'open' | 'closed' | 'all'));
|
||||
break;
|
||||
case 'create_pr':
|
||||
result = await withSession(s => gitea.createPR(s, a.owner as string, a.repo as string, a.head as string, a.base as string, a.title as string, a.body as string | undefined));
|
||||
break;
|
||||
case 'view_pr':
|
||||
result = await withSession(s => gitea.viewPR(s, a.owner as string, a.repo as string, a.number as number));
|
||||
break;
|
||||
case 'comment_pr':
|
||||
result = await withSession(s => gitea.commentPR(s, a.owner as string, a.repo as string, a.number as number, a.body as string));
|
||||
break;
|
||||
case 'merge_pr':
|
||||
result = await withSession(s => gitea.mergePR(s, a.owner as string, a.repo as string, a.number as number));
|
||||
break;
|
||||
case 'close_pr':
|
||||
result = await withSession(s => gitea.closePR(s, a.owner as string, a.repo as string, a.number as number));
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
Reference in New Issue
Block a user