Skip to content

Commit 36a7cb3

Browse files
authored
Merge pull request #51 from angiejones/fix/mcp-spec-compliance
fix: MCP spec compliance — isError, mimeType, image content type
2 parents 8792914 + f830b81 commit 36a7cb3

5 files changed

Lines changed: 46 additions & 22 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,5 @@ npm-debug.log*
4444
# Selenium
4545
geckodriver.log
4646
chromedriver.log
47-
.goose/
47+
.goose/
48+
.beads/

src/lib/server.js

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ server.tool(
123123
};
124124
} catch (e) {
125125
return {
126-
content: [{ type: 'text', text: `Error starting browser: ${e.message}` }]
126+
content: [{ type: 'text', text: `Error starting browser: ${e.message}` }],
127+
isError: true
127128
};
128129
}
129130
}
@@ -144,7 +145,8 @@ server.tool(
144145
};
145146
} catch (e) {
146147
return {
147-
content: [{ type: 'text', text: `Error navigating: ${e.message}` }]
148+
content: [{ type: 'text', text: `Error navigating: ${e.message}` }],
149+
isError: true
148150
};
149151
}
150152
}
@@ -167,7 +169,8 @@ server.tool(
167169
};
168170
} catch (e) {
169171
return {
170-
content: [{ type: 'text', text: `Error finding element: ${e.message}` }]
172+
content: [{ type: 'text', text: `Error finding element: ${e.message}` }],
173+
isError: true
171174
};
172175
}
173176
}
@@ -190,7 +193,8 @@ server.tool(
190193
};
191194
} catch (e) {
192195
return {
193-
content: [{ type: 'text', text: `Error clicking element: ${e.message}` }]
196+
content: [{ type: 'text', text: `Error clicking element: ${e.message}` }],
197+
isError: true
194198
};
195199
}
196200
}
@@ -215,7 +219,8 @@ server.tool(
215219
};
216220
} catch (e) {
217221
return {
218-
content: [{ type: 'text', text: `Error entering text: ${e.message}` }]
222+
content: [{ type: 'text', text: `Error entering text: ${e.message}` }],
223+
isError: true
219224
};
220225
}
221226
}
@@ -238,7 +243,8 @@ server.tool(
238243
};
239244
} catch (e) {
240245
return {
241-
content: [{ type: 'text', text: `Error getting element text: ${e.message}` }]
246+
content: [{ type: 'text', text: `Error getting element text: ${e.message}` }],
247+
isError: true
242248
};
243249
}
244250
}
@@ -262,7 +268,8 @@ server.tool(
262268
};
263269
} catch (e) {
264270
return {
265-
content: [{ type: 'text', text: `Error hovering over element: ${e.message}` }]
271+
content: [{ type: 'text', text: `Error hovering over element: ${e.message}` }],
272+
isError: true
266273
};
267274
}
268275
}
@@ -290,7 +297,8 @@ server.tool(
290297
};
291298
} catch (e) {
292299
return {
293-
content: [{ type: 'text', text: `Error performing drag and drop: ${e.message}` }]
300+
content: [{ type: 'text', text: `Error performing drag and drop: ${e.message}` }],
301+
isError: true
294302
};
295303
}
296304
}
@@ -314,7 +322,8 @@ server.tool(
314322
};
315323
} catch (e) {
316324
return {
317-
content: [{ type: 'text', text: `Error performing double click: ${e.message}` }]
325+
content: [{ type: 'text', text: `Error performing double click: ${e.message}` }],
326+
isError: true
318327
};
319328
}
320329
}
@@ -338,7 +347,8 @@ server.tool(
338347
};
339348
} catch (e) {
340349
return {
341-
content: [{ type: 'text', text: `Error performing right click: ${e.message}` }]
350+
content: [{ type: 'text', text: `Error performing right click: ${e.message}` }],
351+
isError: true
342352
};
343353
}
344354
}
@@ -371,7 +381,8 @@ server.tool(
371381
};
372382
} catch (e) {
373383
return {
374-
content: [{ type: 'text', text: `Error pressing key: ${e.message}` }]
384+
content: [{ type: 'text', text: `Error pressing key: ${e.message}` }],
385+
isError: true
375386
};
376387
}
377388
}
@@ -395,7 +406,8 @@ server.tool(
395406
};
396407
} catch (e) {
397408
return {
398-
content: [{ type: 'text', text: `Error uploading file: ${e.message}` }]
409+
content: [{ type: 'text', text: `Error uploading file: ${e.message}` }],
410+
isError: true
399411
};
400412
}
401413
}
@@ -405,7 +417,7 @@ server.tool(
405417
"take_screenshot",
406418
"captures a screenshot of the current page",
407419
{
408-
outputPath: z.string().optional().describe("Optional path where to save the screenshot. If not provided, returns base64 data.")
420+
outputPath: z.string().optional().describe("Optional path where to save the screenshot. If not provided, returns an image/png content block.")
409421
},
410422
async ({ outputPath }) => {
411423
try {
@@ -420,14 +432,14 @@ server.tool(
420432
} else {
421433
return {
422434
content: [
423-
{ type: 'text', text: 'Screenshot captured as base64:' },
424-
{ type: 'text', text: screenshot }
435+
{ type: 'image', data: screenshot, mimeType: 'image/png' }
425436
]
426437
};
427438
}
428439
} catch (e) {
429440
return {
430-
content: [{ type: 'text', text: `Error taking screenshot: ${e.message}` }]
441+
content: [{ type: 'text', text: `Error taking screenshot: ${e.message}` }],
442+
isError: true
431443
};
432444
}
433445
}
@@ -449,7 +461,8 @@ server.tool(
449461
};
450462
} catch (e) {
451463
return {
452-
content: [{ type: 'text', text: `Error closing session: ${e.message}` }]
464+
content: [{ type: 'text', text: `Error closing session: ${e.message}` }],
465+
isError: true
453466
};
454467
}
455468
}
@@ -462,6 +475,7 @@ server.resource(
462475
async (uri) => ({
463476
contents: [{
464477
uri: uri.href,
478+
mimeType: "text/plain",
465479
text: state.currentSession
466480
? `Active browser session: ${state.currentSession}`
467481
: "No active browser session"

test/browser.test.mjs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ describe('Browser Management', () => {
5050

5151
it('should error when no active session exists', async () => {
5252
const result = await client.callTool('close_session');
53+
assert.strictEqual(result.isError, true, 'Expected isError: true on error response');
5354
const text = getResponseText(result);
5455
assert.ok(
5556
text.includes('Error') || text.includes('No active'),
@@ -71,11 +72,13 @@ describe('Browser Management', () => {
7172
try { await client.callTool('close_session'); } catch { /* ignore */ }
7273
});
7374

74-
it('should capture a screenshot and return base64 data', async () => {
75+
it('should capture a screenshot and return base64 image content', async () => {
7576
const result = await client.callTool('take_screenshot');
76-
assert.ok(result?.content?.length >= 2, 'Should return at least 2 content entries');
77-
const base64 = result.content[1].text;
78-
assert.ok(base64.length > 100, `Expected base64 data, got ${base64.length} chars`);
77+
assert.ok(result?.content?.length >= 1, 'Should return at least 1 content entry');
78+
const imageContent = result.content.find(c => c.type === 'image');
79+
assert.ok(imageContent, 'Should contain an image content entry');
80+
assert.strictEqual(imageContent.mimeType, 'image/png', 'Image mimeType should be image/png');
81+
assert.ok(imageContent.data.length > 100, `Expected base64 data, got ${imageContent.data.length} chars`);
7982
});
8083
});
8184

test/interactions.test.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ describe('Element Interactions', () => {
5252

5353
it('should error when element not found', async () => {
5454
const result = await client.callTool('click_element', { by: 'id', value: 'nonexistent' });
55+
assert.strictEqual(result.isError, true, 'Expected isError: true on error response');
5556
const text = getResponseText(result);
5657
assert.ok(text.includes('Error'), `Expected error, got: ${text}`);
5758
});
@@ -90,6 +91,7 @@ describe('Element Interactions', () => {
9091
value: 'not-an-input',
9192
text: 'should fail',
9293
});
94+
assert.strictEqual(result.isError, true, 'Expected isError: true on error response');
9395
const text = getResponseText(result);
9496
assert.ok(text.includes('Error'), `Expected error sending keys to div, got: ${text}`);
9597
});
@@ -123,6 +125,7 @@ describe('Element Interactions', () => {
123125
by: 'id',
124126
value: 'nonexistent',
125127
});
128+
assert.strictEqual(result.isError, true, 'Expected isError: true on error response');
126129
const text = getResponseText(result);
127130
assert.ok(text.includes('Error'), `Expected error, got: ${text}`);
128131
});
@@ -211,6 +214,7 @@ describe('Element Interactions', () => {
211214

212215
it('should error on unknown key name', async () => {
213216
const result = await client.callTool('press_key', { key: 'FakeKey' });
217+
assert.strictEqual(result.isError, true, 'Expected isError: true on error response');
214218
const text = getResponseText(result);
215219
assert.ok(text.includes('Unknown key name'), `Expected unknown key error, got: ${text}`);
216220
});

test/navigation.test.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ describe('Navigation & Element Locators', () => {
4444
await freshClient.start();
4545
try {
4646
const result = await freshClient.callTool('navigate', { url: 'https://example.com' });
47+
assert.strictEqual(result.isError, true, 'Expected isError: true on error response');
4748
const text = getResponseText(result);
4849
assert.ok(
4950
text.includes('Error') || text.includes('No active'),
@@ -154,6 +155,7 @@ describe('Navigation & Element Locators', () => {
154155

155156
it('should error when element not found', async () => {
156157
const result = await client.callTool('find_element', { by: 'id', value: 'nonexistent' });
158+
assert.strictEqual(result.isError, true, 'Expected isError: true on error response');
157159
const text = getResponseText(result);
158160
assert.ok(text.includes('Error'), `Expected error, got: ${text}`);
159161
});

0 commit comments

Comments
 (0)