cli / mcp for bitbucket
1import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
3import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
5import { registerAllTools } from '../../tools';
6
7vi.mock('@bitbucket-tool/core', () => ({
8 listCommits: vi.fn(),
9 getCommit: vi.fn(),
10}));
11
12import { getCommit, listCommits } from '@bitbucket-tool/core';
13
14const ws = 'test-workspace';
15const repo = 'test-repo';
16
17const callTool = async (client: Client, name: string, args: Record<string, unknown>) =>
18 client.callTool({ name, arguments: args });
19
20const textOf = (result: Awaited<ReturnType<Client['callTool']>>) =>
21 (result.content as Array<{ type: string; text: string }>)[0].text;
22
23describe('Commit tools', () => {
24 let client: Client;
25 let server: McpServer;
26
27 beforeAll(async () => {
28 const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
29 server = new McpServer({ name: 'test', version: '0.0.1' });
30 client = new Client({ name: 'test-client', version: '0.0.1' });
31
32 registerAllTools(server);
33
34 await server.connect(serverTransport);
35 await client.connect(clientTransport);
36 });
37
38 afterAll(async () => {
39 await client.close();
40 await server.close();
41 });
42
43 beforeEach(() => {
44 vi.clearAllMocks();
45 });
46
47 describe('list_commits', () => {
48 it('passes params to core and returns JSON response', async () => {
49 const commits = [{ hash: 'abc123', message: 'initial commit' }];
50 vi.mocked(listCommits).mockResolvedValue({ ok: true, data: commits as never });
51
52 const result = await callTool(client, 'list_commits', {
53 workspace: ws,
54 repo_slug: repo,
55 page: 1,
56 pagelen: 30,
57 });
58
59 expect(listCommits).toHaveBeenCalledWith({
60 workspace: ws,
61 repoSlug: repo,
62 page: 1,
63 pagelen: 30,
64 });
65 expect(result.isError).toBeFalsy();
66 expect(JSON.parse(textOf(result))).toEqual(commits);
67 });
68
69 it('returns error response on failure', async () => {
70 vi.mocked(listCommits).mockResolvedValue({
71 ok: false,
72 error: { status: 404, message: 'Repository not found' },
73 });
74
75 const result = await callTool(client, 'list_commits', {
76 workspace: ws,
77 repo_slug: repo,
78 });
79
80 expect(result.isError).toBe(true);
81 expect(textOf(result)).toBe('Repository not found (404)');
82 });
83 });
84
85 describe('get_commit', () => {
86 it('passes params to core and returns JSON response', async () => {
87 const commit = { hash: 'abc123', message: 'feat: add feature', author: { raw: 'Test' } };
88 vi.mocked(getCommit).mockResolvedValue({ ok: true, data: commit as never });
89
90 const result = await callTool(client, 'get_commit', {
91 workspace: ws,
92 repo_slug: repo,
93 commit: 'abc123',
94 });
95
96 expect(getCommit).toHaveBeenCalledWith({
97 workspace: ws,
98 repoSlug: repo,
99 commit: 'abc123',
100 });
101 expect(JSON.parse(textOf(result))).toEqual(commit);
102 });
103
104 it('returns error on failure', async () => {
105 vi.mocked(getCommit).mockResolvedValue({
106 ok: false,
107 error: { status: 404, message: 'Commit not found' },
108 });
109
110 const result = await callTool(client, 'get_commit', {
111 workspace: ws,
112 repo_slug: repo,
113 commit: 'nonexistent',
114 });
115
116 expect(result.isError).toBe(true);
117 expect(textOf(result)).toBe('Commit not found (404)');
118 });
119 });
120});