-
Notifications
You must be signed in to change notification settings - Fork 152
Expand file tree
/
Copy pathprotocol.test.ts
More file actions
212 lines (176 loc) Β· 7.02 KB
/
protocol.test.ts
File metadata and controls
212 lines (176 loc) Β· 7.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
import { describe, it, expect, afterAll } from 'bun:test';
import { resolve } from 'node:path';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
const BLANK_DOCX = resolve(import.meta.dir, '../../../../shared/common/data/blank.docx');
const SERVER_ENTRY = resolve(import.meta.dir, '../index.ts');
// 4 lifecycle + 10 intent tools from the generated catalog
const EXPECTED_TOOLS = [
// Lifecycle
'superdoc_open',
'superdoc_attach',
'superdoc_save',
'superdoc_close',
// Intent tools (from catalog.json)
'superdoc_get_content',
'superdoc_edit',
'superdoc_format',
'superdoc_create',
'superdoc_list',
'superdoc_comment',
'superdoc_track_changes',
'superdoc_search',
'superdoc_mutations',
'superdoc_table',
];
function textContent(result: Awaited<ReturnType<Client['callTool']>>): string {
const content = 'content' in result ? result.content : [];
const first = (content as Array<{ type: string; text?: string }>)[0];
return first?.text ?? '';
}
function parseContent(result: Awaited<ReturnType<Client['callTool']>>): unknown {
return JSON.parse(textContent(result));
}
describe('MCP protocol integration', () => {
let client: Client;
let transport: StdioClientTransport;
// Connect once for all tests β spawns the server subprocess
const ready = (async () => {
transport = new StdioClientTransport({
command: 'bun',
args: ['run', SERVER_ENTRY],
stderr: 'pipe',
});
client = new Client({ name: 'test-client', version: '1.0.0' });
await client.connect(transport);
})();
afterAll(async () => {
await transport?.close();
});
it('connects and lists all expected tools', async () => {
await ready;
const { tools } = await client.listTools();
const names = tools.map((t) => t.name).sort();
expect(names).toEqual([...EXPECTED_TOOLS].sort());
});
it('tools have required annotations', async () => {
await ready;
const { tools } = await client.listTools();
for (const tool of tools) {
expect(tool.annotations).toBeDefined();
expect(typeof tool.annotations!.readOnlyHint).toBe('boolean');
}
});
it('intent tools have action enum in schema', async () => {
await ready;
const { tools } = await client.listTools();
// Multi-action intent tools should have an "action" property with an enum
// superdoc_attach, like superdoc_open, is a session-creating lifecycle tool β no action enum.
const multiActionTools = tools.filter(
(t) =>
!['superdoc_open', 'superdoc_attach', 'superdoc_save', 'superdoc_close', 'superdoc_search'].includes(t.name),
);
for (const tool of multiActionTools) {
const schema = tool.inputSchema as { properties?: Record<string, { enum?: string[] }> };
expect(schema.properties?.action).toBeDefined();
expect(schema.properties!.action.enum).toBeArray();
expect(schema.properties!.action.enum!.length).toBeGreaterThan(0);
}
});
it('intent tools have session_id in schema', async () => {
await ready;
const { tools } = await client.listTools();
// All intent tools (not session-creating lifecycle tools) should require session_id.
// superdoc_open and superdoc_attach both produce a session_id rather than consuming one.
const intentTools = tools.filter(
(t) => !['superdoc_open', 'superdoc_attach', 'superdoc_save', 'superdoc_close'].includes(t.name),
);
for (const tool of intentTools) {
const schema = tool.inputSchema as { properties?: Record<string, unknown>; required?: string[] };
expect(schema.properties?.session_id).toBeDefined();
expect(schema.required).toContain('session_id');
}
});
it('open β get_content β close workflow', async () => {
await ready;
// Open
const openResult = await client.callTool({ name: 'superdoc_open', arguments: { path: BLANK_DOCX } });
const opened = parseContent(openResult) as { session_id: string; filePath: string };
expect(opened.session_id).toBeString();
expect(opened.filePath).toBe(BLANK_DOCX);
const sid = opened.session_id;
// Get content as text
const textResult = await client.callTool({
name: 'superdoc_get_content',
arguments: { session_id: sid, action: 'text' },
});
expect(textContent(textResult)).toBeDefined();
// Get content as info
const infoResult = await client.callTool({
name: 'superdoc_get_content',
arguments: { session_id: sid, action: 'info' },
});
expect(textContent(infoResult)).toBeTruthy();
// Close
const closeResult = await client.callTool({ name: 'superdoc_close', arguments: { session_id: sid } });
const closed = parseContent(closeResult) as { closed: boolean };
expect(closed.closed).toBe(true);
});
it('open β create β search β save β close workflow', async () => {
await ready;
// Open
const openResult = await client.callTool({ name: 'superdoc_open', arguments: { path: BLANK_DOCX } });
const { session_id: sid } = parseContent(openResult) as { session_id: string };
// Create a paragraph
const createResult = await client.callTool({
name: 'superdoc_create',
arguments: { session_id: sid, action: 'paragraph', text: 'MCP integration test' },
});
expect(textContent(createResult)).toBeTruthy();
// Search for it
const searchResult = await client.callTool({
name: 'superdoc_search',
arguments: {
session_id: sid,
select: { type: 'text', pattern: 'MCP integration' },
},
});
const found = parseContent(searchResult) as { matches: unknown[]; total: number };
expect(found.total).toBeGreaterThan(0);
// Save to temp path
const tmpPath = resolve(import.meta.dir, '../../../../tmp-protocol-test.docx');
const saveResult = await client.callTool({
name: 'superdoc_save',
arguments: { session_id: sid, out: tmpPath },
});
const saved = parseContent(saveResult) as { path: string; byteLength: number };
expect(saved.byteLength).toBeGreaterThan(0);
// Close
await client.callTool({ name: 'superdoc_close', arguments: { session_id: sid } });
// Clean up temp file
const { unlink } = await import('node:fs/promises');
await unlink(tmpPath).catch(() => {});
});
it('returns isError for invalid session', async () => {
await ready;
const result = await client.callTool({
name: 'superdoc_search',
arguments: {
session_id: 'nonexistent',
select: { type: 'text', pattern: 'test' },
},
});
expect(result).toHaveProperty('isError', true);
expect(textContent(result)).toContain('No open session');
});
it('creates a blank document when file does not exist', async () => {
await ready;
const result = await client.callTool({
name: 'superdoc_open',
arguments: { path: '/nonexistent/file.docx' },
});
expect(result).not.toHaveProperty('isError');
const body = JSON.parse(textContent(result));
expect(body).toHaveProperty('session_id');
});
});