feat: add list_pull_requests tool with filtering and pagination support
This commit is contained in:
parent
b1d758646c
commit
208366f9f5
2 changed files with 229 additions and 15 deletions
24
README.md
24
README.md
|
@ -6,12 +6,13 @@ An MCP (Model Context Protocol) server that provides tools for interacting with
|
||||||
|
|
||||||
Currently implemented:
|
Currently implemented:
|
||||||
- `get_pull_request` - Retrieve detailed information about a pull request
|
- `get_pull_request` - Retrieve detailed information about a pull request
|
||||||
|
- `list_pull_requests` - List pull requests with filters (state, author, pagination)
|
||||||
|
|
||||||
Planned features:
|
Planned features:
|
||||||
- `create_pull_request` - Create new pull requests
|
- `create_pull_request` - Create new pull requests
|
||||||
- `list_pull_requests` - List pull requests with filters
|
|
||||||
- `update_pull_request` - Update PR details
|
- `update_pull_request` - Update PR details
|
||||||
- `merge_pull_request` - Merge pull requests
|
- `merge_pull_request` - Merge pull requests
|
||||||
|
- `delete_branch` - Delete branches
|
||||||
- And more...
|
- And more...
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
@ -117,6 +118,27 @@ Returns detailed information about the pull request including:
|
||||||
- Links to web UI and diff
|
- Links to web UI and diff
|
||||||
- And more...
|
- And more...
|
||||||
|
|
||||||
|
### List Pull Requests
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
"tool": "list_pull_requests",
|
||||||
|
"arguments": {
|
||||||
|
"workspace": "PROJ", // Required - your project key
|
||||||
|
"repository": "my-repo",
|
||||||
|
"state": "OPEN", // Optional: OPEN, MERGED, DECLINED, ALL (default: OPEN)
|
||||||
|
"author": "username", // Optional: filter by author
|
||||||
|
"limit": 25, // Optional: max results per page (default: 25)
|
||||||
|
"start": 0 // Optional: pagination start index (default: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns a paginated list of pull requests with:
|
||||||
|
- Array of pull requests with same details as get_pull_request
|
||||||
|
- Total count of matching PRs
|
||||||
|
- Pagination info (has_more, next_start)
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
- `npm run dev` - Watch mode for development
|
- `npm run dev` - Watch mode for development
|
||||||
|
|
202
src/index.ts
202
src/index.ts
|
@ -169,6 +169,25 @@ const isGetPullRequestArgs = (
|
||||||
typeof args.repository === 'string' &&
|
typeof args.repository === 'string' &&
|
||||||
typeof args.pull_request_id === 'number';
|
typeof args.pull_request_id === 'number';
|
||||||
|
|
||||||
|
const isListPullRequestsArgs = (
|
||||||
|
args: any
|
||||||
|
): args is {
|
||||||
|
workspace: string;
|
||||||
|
repository: string;
|
||||||
|
state?: string;
|
||||||
|
author?: string;
|
||||||
|
limit?: number;
|
||||||
|
start?: number;
|
||||||
|
} =>
|
||||||
|
typeof args === 'object' &&
|
||||||
|
args !== null &&
|
||||||
|
typeof args.workspace === 'string' &&
|
||||||
|
typeof args.repository === 'string' &&
|
||||||
|
(args.state === undefined || typeof args.state === 'string') &&
|
||||||
|
(args.author === undefined || typeof args.author === 'string') &&
|
||||||
|
(args.limit === undefined || typeof args.limit === 'number') &&
|
||||||
|
(args.start === undefined || typeof args.start === 'number');
|
||||||
|
|
||||||
class BitbucketMCPServer {
|
class BitbucketMCPServer {
|
||||||
private server: Server;
|
private server: Server;
|
||||||
private axiosInstance: AxiosInstance;
|
private axiosInstance: AxiosInstance;
|
||||||
|
@ -295,7 +314,7 @@ class BitbucketMCPServer {
|
||||||
properties: {
|
properties: {
|
||||||
workspace: {
|
workspace: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Bitbucket workspace/project key (e.g., "JBIZ")',
|
description: 'Bitbucket workspace/project key (e.g., "PROJ")',
|
||||||
},
|
},
|
||||||
repository: {
|
repository: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
@ -309,26 +328,69 @@ class BitbucketMCPServer {
|
||||||
required: ['workspace', 'repository', 'pull_request_id'],
|
required: ['workspace', 'repository', 'pull_request_id'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'list_pull_requests',
|
||||||
|
description: 'List pull requests for a repository with optional filters',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
workspace: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Bitbucket workspace/project key (e.g., "PROJ")',
|
||||||
|
},
|
||||||
|
repository: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Repository slug (e.g., "my-repo")',
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Filter by PR state: OPEN, MERGED, DECLINED, ALL (default: OPEN)',
|
||||||
|
enum: ['OPEN', 'MERGED', 'DECLINED', 'ALL'],
|
||||||
|
},
|
||||||
|
author: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Filter by author username',
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Maximum number of PRs to return (default: 25)',
|
||||||
|
},
|
||||||
|
start: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Start index for pagination (default: 0)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['workspace', 'repository'],
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Handle tool calls
|
// Handle tool calls
|
||||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
if (request.params.name !== 'get_pull_request') {
|
switch (request.params.name) {
|
||||||
|
case 'get_pull_request':
|
||||||
|
return this.handleGetPullRequest(request.params.arguments);
|
||||||
|
case 'list_pull_requests':
|
||||||
|
return this.handleListPullRequests(request.params.arguments);
|
||||||
|
default:
|
||||||
throw new McpError(
|
throw new McpError(
|
||||||
ErrorCode.MethodNotFound,
|
ErrorCode.MethodNotFound,
|
||||||
`Unknown tool: ${request.params.name}`
|
`Unknown tool: ${request.params.name}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!isGetPullRequestArgs(request.params.arguments)) {
|
private async handleGetPullRequest(args: any) {
|
||||||
|
if (!isGetPullRequestArgs(args)) {
|
||||||
throw new McpError(
|
throw new McpError(
|
||||||
ErrorCode.InvalidParams,
|
ErrorCode.InvalidParams,
|
||||||
'Invalid arguments for get_pull_request'
|
'Invalid arguments for get_pull_request'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { workspace, repository, pull_request_id } = request.params.arguments;
|
const { workspace, repository, pull_request_id } = args;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Different API paths for Server vs Cloud
|
// Different API paths for Server vs Cloud
|
||||||
|
@ -399,7 +461,137 @@ class BitbucketMCPServer {
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
private async handleListPullRequests(args: any) {
|
||||||
|
if (!isListPullRequestsArgs(args)) {
|
||||||
|
throw new McpError(
|
||||||
|
ErrorCode.InvalidParams,
|
||||||
|
'Invalid arguments for list_pull_requests'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { workspace, repository, state = 'OPEN', author, limit = 25, start = 0 } = args;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let apiPath: string;
|
||||||
|
let params: any = {};
|
||||||
|
|
||||||
|
if (this.isServer) {
|
||||||
|
// Bitbucket Server API
|
||||||
|
apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests`;
|
||||||
|
params = {
|
||||||
|
state: state === 'ALL' ? undefined : state,
|
||||||
|
limit,
|
||||||
|
start,
|
||||||
|
};
|
||||||
|
if (author) {
|
||||||
|
params['username'] = author;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Bitbucket Cloud API
|
||||||
|
apiPath = `/repositories/${workspace}/${repository}/pullrequests`;
|
||||||
|
params = {
|
||||||
|
state: state === 'ALL' ? undefined : state,
|
||||||
|
pagelen: limit,
|
||||||
|
page: Math.floor(start / limit) + 1,
|
||||||
|
};
|
||||||
|
if (author) {
|
||||||
|
params['q'] = `author.username="${author}"`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(`[DEBUG] Listing PRs from: ${BITBUCKET_BASE_URL}${apiPath}`);
|
||||||
|
console.error(`[DEBUG] Params:`, params);
|
||||||
|
|
||||||
|
const response = await this.axiosInstance.get(apiPath, { params });
|
||||||
|
const data = response.data;
|
||||||
|
|
||||||
|
// Format the response
|
||||||
|
let pullRequests: any[] = [];
|
||||||
|
let totalCount = 0;
|
||||||
|
let nextPageStart = null;
|
||||||
|
|
||||||
|
if (this.isServer) {
|
||||||
|
// Bitbucket Server response
|
||||||
|
pullRequests = (data.values || []).map((pr: BitbucketServerPullRequest) =>
|
||||||
|
this.formatServerResponse(pr)
|
||||||
|
);
|
||||||
|
totalCount = data.size || 0;
|
||||||
|
if (!data.isLastPage && data.nextPageStart !== undefined) {
|
||||||
|
nextPageStart = data.nextPageStart;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Bitbucket Cloud response
|
||||||
|
pullRequests = (data.values || []).map((pr: BitbucketCloudPullRequest) =>
|
||||||
|
this.formatCloudResponse(pr)
|
||||||
|
);
|
||||||
|
totalCount = data.size || 0;
|
||||||
|
if (data.next) {
|
||||||
|
nextPageStart = start + limit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify({
|
||||||
|
pull_requests: pullRequests,
|
||||||
|
total_count: totalCount,
|
||||||
|
start,
|
||||||
|
limit,
|
||||||
|
has_more: nextPageStart !== null,
|
||||||
|
next_start: nextPageStart,
|
||||||
|
}, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const status = error.response?.status;
|
||||||
|
const message = error.response?.data?.errors?.[0]?.message ||
|
||||||
|
error.response?.data?.error?.message ||
|
||||||
|
error.response?.data?.message ||
|
||||||
|
error.message;
|
||||||
|
|
||||||
|
console.error(`[DEBUG] API Error: ${status} - ${message}`);
|
||||||
|
console.error(`[DEBUG] Full error response:`, error.response?.data);
|
||||||
|
|
||||||
|
if (status === 404) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: `Repository not found: ${workspace}/${repository}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
} else if (status === 401) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: `Authentication failed. Please check your ${this.isServer ? 'BITBUCKET_TOKEN' : 'BITBUCKET_USERNAME and BITBUCKET_APP_PASSWORD'}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: `Bitbucket API error: ${message}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
|
|
Loading…
Reference in a new issue