@sdods/mcp 0.3.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser/config.d.ts +64 -0
- package/dist/browser/config.js +100 -0
- package/dist/browser/index.d.ts +8 -0
- package/dist/browser/index.js +8 -0
- package/dist/browser/manifest.d.ts +35 -0
- package/dist/browser/manifest.js +105 -0
- package/dist/browser/playwright-driver.d.ts +18 -0
- package/dist/browser/playwright-driver.js +69 -0
- package/dist/browser/policy.d.ts +23 -0
- package/dist/browser/policy.js +30 -0
- package/dist/browser/session.d.ts +57 -0
- package/dist/browser/session.js +145 -0
- package/dist/browser/shapes.d.ts +254 -0
- package/dist/browser/shapes.js +416 -0
- package/dist/browser/types.d.ts +61 -0
- package/dist/browser/types.js +2 -0
- package/dist/install/index.d.ts +9 -0
- package/dist/install/index.js +11 -2
- package/dist/prompts/index.js +1 -1
- package/dist/registry/registry.d.ts +18 -1
- package/dist/registry/registry.js +14 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.js +24 -31
- package/dist/tools/browser.d.ts +3 -0
- package/dist/tools/browser.js +191 -0
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.js +3 -1
- package/dist/tools/run.d.ts +1 -0
- package/dist/tools/run.js +2 -4
- package/package.json +4 -2
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { BrowserName } from '@sdods/contracts';
|
|
2
|
+
/**
|
|
3
|
+
* The seam a different browser backend would plug into.
|
|
4
|
+
*
|
|
5
|
+
* There is one implementation today (`playwright-driver.ts`, wrapping the Playwright MCP server
|
|
6
|
+
* bundled with the pinned Playwright). It exists as an interface because the obvious alternative —
|
|
7
|
+
* browser-use — is a Python project whose npm namesake is an unrelated single-maintainer package,
|
|
8
|
+
* so adopting it is a dependency decision this repo has not made. When it is made, it is a new
|
|
9
|
+
* file here rather than an edit to 42 tool definitions.
|
|
10
|
+
*/
|
|
11
|
+
export interface BrowserDriver {
|
|
12
|
+
/** Tool names the backend actually offers, for the drift check. */
|
|
13
|
+
listTools(): Promise<string[]>;
|
|
14
|
+
call(tool: string, args: Record<string, unknown>): Promise<DriverResult>;
|
|
15
|
+
close(): Promise<void>;
|
|
16
|
+
/** What the backend reported about itself, for diagnostics. */
|
|
17
|
+
info(): {
|
|
18
|
+
server: string;
|
|
19
|
+
version: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export interface DriverResult {
|
|
23
|
+
text: string;
|
|
24
|
+
images: Array<{
|
|
25
|
+
data: string;
|
|
26
|
+
mimeType: string;
|
|
27
|
+
}>;
|
|
28
|
+
isError: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** What a caller may choose when opening a session; everything else comes from project config. */
|
|
31
|
+
export interface BrowserSessionSpec {
|
|
32
|
+
sessionId: string;
|
|
33
|
+
project: string;
|
|
34
|
+
env?: string;
|
|
35
|
+
/** `@user:<role>` — reuses the storage state `sdods auth capture` wrote for that role. */
|
|
36
|
+
role?: string;
|
|
37
|
+
browser?: BrowserName;
|
|
38
|
+
headed?: boolean;
|
|
39
|
+
device?: string;
|
|
40
|
+
viewport?: {
|
|
41
|
+
width: number;
|
|
42
|
+
height: number;
|
|
43
|
+
};
|
|
44
|
+
caps?: Array<'vision' | 'pdf' | 'devtools'>;
|
|
45
|
+
/** `none` keeps large accessibility snapshots out of every response. */
|
|
46
|
+
snapshotMode?: 'full' | 'none';
|
|
47
|
+
images?: 'allow' | 'omit';
|
|
48
|
+
}
|
|
49
|
+
export interface SessionInfo {
|
|
50
|
+
sessionId: string;
|
|
51
|
+
project: string;
|
|
52
|
+
env?: string;
|
|
53
|
+
role?: string;
|
|
54
|
+
browser: BrowserName;
|
|
55
|
+
headed: boolean;
|
|
56
|
+
outputDir: string;
|
|
57
|
+
openedAt: string;
|
|
58
|
+
lastUsed: string;
|
|
59
|
+
server: string;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=types.d.ts.map
|
package/dist/install/index.d.ts
CHANGED
|
@@ -13,6 +13,15 @@ export interface SnippetOptions {
|
|
|
13
13
|
withPlaywright?: boolean;
|
|
14
14
|
}
|
|
15
15
|
export declare function serverEntry(o: SnippetOptions, client: McpClient): Record<string, unknown>;
|
|
16
|
+
/**
|
|
17
|
+
* The raw upstream browser server.
|
|
18
|
+
*
|
|
19
|
+
* No longer registered by default: SDODS now wraps every one of its tools as `browser_*`, bound to
|
|
20
|
+
* a project and environment, with credential headers redacted and the two code-execution tools
|
|
21
|
+
* behind a scope and a switch. Registering the raw server alongside would leave an ungoverned path
|
|
22
|
+
* to the same browser, which is the thing the wrapper exists to close. Kept for
|
|
23
|
+
* `--with-playwright`, for anyone who wants upstream directly and accepts that.
|
|
24
|
+
*/
|
|
16
25
|
export declare function playwrightEntry(): Record<string, unknown>;
|
|
17
26
|
/** `codex mcp add` / `claude mcp add` command lines for the same configuration. */
|
|
18
27
|
export declare function cliCommands(o: SnippetOptions): {
|
package/dist/install/index.js
CHANGED
|
@@ -20,6 +20,15 @@ export function serverEntry(o, client) {
|
|
|
20
20
|
}
|
|
21
21
|
return { command: o.command ?? 'npx', args: stdioArgs(o) };
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* The raw upstream browser server.
|
|
25
|
+
*
|
|
26
|
+
* No longer registered by default: SDODS now wraps every one of its tools as `browser_*`, bound to
|
|
27
|
+
* a project and environment, with credential headers redacted and the two code-execution tools
|
|
28
|
+
* behind a scope and a switch. Registering the raw server alongside would leave an ungoverned path
|
|
29
|
+
* to the same browser, which is the thing the wrapper exists to close. Kept for
|
|
30
|
+
* `--with-playwright`, for anyone who wants upstream directly and accepts that.
|
|
31
|
+
*/
|
|
23
32
|
export function playwrightEntry() {
|
|
24
33
|
return { command: 'npx', args: ['playwright', 'mcp', '--headless'] };
|
|
25
34
|
}
|
|
@@ -42,7 +51,7 @@ export function codexTomlEntries(o) {
|
|
|
42
51
|
? { url: o.httpUrl, bearer_token_env_var: 'SDODS_TOKEN' }
|
|
43
52
|
: { command: o.command ?? 'npx', args: stdioArgs(o) };
|
|
44
53
|
const out = { sdods };
|
|
45
|
-
if (o.withPlaywright
|
|
54
|
+
if (o.withPlaywright === true)
|
|
46
55
|
out.playwright = playwrightEntry();
|
|
47
56
|
return out;
|
|
48
57
|
}
|
|
@@ -50,7 +59,7 @@ export function codexConfigPath() {
|
|
|
50
59
|
return join(process.env.CODEX_HOME ?? join(homedir(), '.codex'), 'config.toml');
|
|
51
60
|
}
|
|
52
61
|
export function snippets(o) {
|
|
53
|
-
const pw = o.withPlaywright ===
|
|
62
|
+
const pw = o.withPlaywright === true ? { playwright: playwrightEntry() } : {};
|
|
54
63
|
const cli = cliCommands(o);
|
|
55
64
|
return {
|
|
56
65
|
claude: {
|
package/dist/prompts/index.js
CHANGED
|
@@ -29,7 +29,7 @@ Goal: produce \`docs/test-plans/<name>.md\` for the requested feature area.
|
|
|
29
29
|
|
|
30
30
|
Method:
|
|
31
31
|
1. Read the project config (project_get_config) and existing features (feature_list) to avoid duplicates.
|
|
32
|
-
2. Explore the application:
|
|
32
|
+
2. Explore the application: open a session with browser_session_open, then navigate the relevant routes and take ARIA snapshots with browser_navigate and browser_snapshot; otherwise read source roots and the OpenAPI spec (analyze_project, analyze_routes).
|
|
33
33
|
3. Write independent scenarios grouped by module. For each: title, preconditions, Gherkin steps (reuse step phrasing from step_list), the layer and suite tag, data needs (@data:/@user:), negative cases, and which existing steps already cover it.
|
|
34
34
|
4. Include an "Out of scope / risks" section and an estimate of new steps needed.
|
|
35
35
|
5. Save the plan through the proposal tools; do not modify features directly.`,
|
|
@@ -2,7 +2,7 @@ import { z, type ZodRawShape } from 'zod';
|
|
|
2
2
|
import { type Scope } from '@sdods/contracts';
|
|
3
3
|
export declare const DOCS_BASE_URL = "https://docs.sdods.com";
|
|
4
4
|
export type ToolAccess = 'read' | 'run' | 'write';
|
|
5
|
-
export type Capability = 'core' | 'analyze' | 'run' | 'data' | 'agents' | 'issues' | 'schedules';
|
|
5
|
+
export type Capability = 'core' | 'analyze' | 'run' | 'data' | 'browser' | 'agents' | 'issues' | 'schedules';
|
|
6
6
|
export declare const ALL_CAPABILITIES: Capability[];
|
|
7
7
|
export declare const DEFAULT_CAPABILITIES: Capability[];
|
|
8
8
|
export interface Principal {
|
|
@@ -31,6 +31,18 @@ export interface ToolContext {
|
|
|
31
31
|
}) => void;
|
|
32
32
|
signal?: AbortSignal;
|
|
33
33
|
logger: ToolLogger;
|
|
34
|
+
/**
|
|
35
|
+
* Live browser sessions for this connection. Present only when the `browser` capability is on.
|
|
36
|
+
*
|
|
37
|
+
* It hangs off the context rather than living in a module map so that sessions belong to the
|
|
38
|
+
* principal that opened them: the HTTP transport builds one context per MCP session, so a
|
|
39
|
+
* second principal cannot name the first one's session id and drive their logged-in browser.
|
|
40
|
+
*/
|
|
41
|
+
browser?: BrowserSessions;
|
|
42
|
+
}
|
|
43
|
+
/** The subset of the session manager the registry needs to know about, to avoid a cycle. */
|
|
44
|
+
export interface BrowserSessions {
|
|
45
|
+
closeAll(): Promise<void>;
|
|
34
46
|
}
|
|
35
47
|
export interface ToolAnnotations {
|
|
36
48
|
readOnlyHint?: boolean;
|
|
@@ -60,6 +72,11 @@ export interface SdodsTool<S extends ZodRawShape = ZodRawShape> {
|
|
|
60
72
|
domain: string;
|
|
61
73
|
capability: Capability;
|
|
62
74
|
annotations?: ToolAnnotations;
|
|
75
|
+
/**
|
|
76
|
+
* Explicit scope, overriding the access/domain derivation. Lets one tool in a family sit behind
|
|
77
|
+
* a stricter scope than its siblings without inventing a capability for it.
|
|
78
|
+
*/
|
|
79
|
+
scope?: Scope;
|
|
63
80
|
docsPath?: string;
|
|
64
81
|
handler: (args: z.infer<z.ZodObject<S>>, ctx: ToolContext) => Promise<ToolResult>;
|
|
65
82
|
}
|
|
@@ -6,15 +6,28 @@ export const ALL_CAPABILITIES = [
|
|
|
6
6
|
'analyze',
|
|
7
7
|
'run',
|
|
8
8
|
'data',
|
|
9
|
+
'browser',
|
|
9
10
|
'agents',
|
|
10
11
|
'issues',
|
|
11
12
|
'schedules',
|
|
12
13
|
];
|
|
13
|
-
|
|
14
|
+
// `browser` is on by default: the agents that need a browser used to get one from a separately
|
|
15
|
+
// registered Playwright server, and that registration is being removed. Leaving it opt-in would
|
|
16
|
+
// take the browser away from the planner, generator and healer rather than governing it.
|
|
17
|
+
export const DEFAULT_CAPABILITIES = [
|
|
18
|
+
'core',
|
|
19
|
+
'analyze',
|
|
20
|
+
'run',
|
|
21
|
+
'data',
|
|
22
|
+
'browser',
|
|
23
|
+
'schedules',
|
|
24
|
+
];
|
|
14
25
|
export function defineTool(tool) {
|
|
15
26
|
return tool;
|
|
16
27
|
}
|
|
17
28
|
export function requiredScope(tool) {
|
|
29
|
+
if (tool.scope)
|
|
30
|
+
return tool.scope;
|
|
18
31
|
// agents has no read/write pair: run → agents:run, review/apply → agents:review
|
|
19
32
|
if (tool.domain === 'agents')
|
|
20
33
|
return tool.access === 'run' ? 'agents:run' : 'agents:review';
|
package/dist/server.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export declare function buildSdodsMcpServer(opts?: BuildServerOptions): {
|
|
|
24
24
|
ctx: ToolContext;
|
|
25
25
|
registry: ToolRegistry;
|
|
26
26
|
toolCount: number;
|
|
27
|
+
/** Closes the server AND any browser it started. A leaked headless browser outlives the run. */
|
|
28
|
+
close: () => Promise<void>;
|
|
27
29
|
};
|
|
28
30
|
export declare function registerPrompts(server: McpServer, ctx: ToolContext): void;
|
|
29
31
|
//# sourceMappingURL=server.d.ts.map
|
package/dist/server.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { McpServer } from '@modelcontextprotocol/server';
|
|
3
|
+
import { SCOPES } from '@sdods/contracts';
|
|
4
|
+
import { BrowserSessionManager } from './browser/session.js';
|
|
3
5
|
import { cliOrNote } from './cli.js';
|
|
4
6
|
import { findRepoRoot } from './cli.js';
|
|
5
7
|
import { toMcpServer } from './registry/adapters.js';
|
|
@@ -46,47 +48,29 @@ export const LOCAL_ADMIN = { name: 'local', scopes: ['*'], via: 'stdio' };
|
|
|
46
48
|
export function buildToolContext(opts = {}) {
|
|
47
49
|
const rootDir = opts.rootDir ?? findRepoRoot(opts.cwd ?? process.cwd());
|
|
48
50
|
const principal = opts.principal ?? LOCAL_ADMIN;
|
|
51
|
+
const caps = parseCaps(opts.caps);
|
|
49
52
|
return {
|
|
50
53
|
rootDir,
|
|
51
54
|
cwd: opts.cwd ?? rootDir,
|
|
52
55
|
principal: principal.scopes.includes('*') ? { ...principal, scopes: allScopes() } : principal,
|
|
53
|
-
caps
|
|
56
|
+
caps,
|
|
54
57
|
project: opts.project,
|
|
55
58
|
env: opts.env,
|
|
56
59
|
onProgress: opts.onProgress,
|
|
57
60
|
logger: opts.logger ?? stderrLogger(),
|
|
61
|
+
browser: caps.has('browser') ? new BrowserSessionManager({ rootDir }) : undefined,
|
|
58
62
|
};
|
|
59
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Every scope, for a principal that carries `*`.
|
|
66
|
+
*
|
|
67
|
+
* This used to be a hand-copied list with a comment about avoiding a static dependency on the
|
|
68
|
+
* scope shape — but the registry imports from @sdods/contracts two files away, so the copy bought
|
|
69
|
+
* nothing and could only drift. A scope missing here is not a visible bug: it silently withholds
|
|
70
|
+
* tools from an admin.
|
|
71
|
+
*/
|
|
60
72
|
function allScopes() {
|
|
61
|
-
|
|
62
|
-
return [
|
|
63
|
-
'orgs:read',
|
|
64
|
-
'orgs:admin',
|
|
65
|
-
'workspaces:read',
|
|
66
|
-
'workspaces:write',
|
|
67
|
-
'projects:read',
|
|
68
|
-
'projects:write',
|
|
69
|
-
'envs:read',
|
|
70
|
-
'envs:write',
|
|
71
|
-
'datasets:read',
|
|
72
|
-
'datasets:write',
|
|
73
|
-
'runs:read',
|
|
74
|
-
'runs:write',
|
|
75
|
-
'runs:ingest',
|
|
76
|
-
'artifacts:read',
|
|
77
|
-
'features:read',
|
|
78
|
-
'features:write',
|
|
79
|
-
'agents:run',
|
|
80
|
-
'agents:review',
|
|
81
|
-
'integrations:read',
|
|
82
|
-
'integrations:write',
|
|
83
|
-
'schedules:read',
|
|
84
|
-
'schedules:write',
|
|
85
|
-
'processes:read',
|
|
86
|
-
'processes:write',
|
|
87
|
-
'users:admin',
|
|
88
|
-
'audit:read',
|
|
89
|
-
];
|
|
73
|
+
return [...SCOPES];
|
|
90
74
|
}
|
|
91
75
|
/** Build a fully wired MCP server (tools + resources + prompts) for a context. */
|
|
92
76
|
export function buildSdodsMcpServer(opts = {}) {
|
|
@@ -99,7 +83,16 @@ export function buildSdodsMcpServer(opts = {}) {
|
|
|
99
83
|
const toolCount = toMcpServer(registry, server, ctx);
|
|
100
84
|
registerResources(server, ctx);
|
|
101
85
|
registerPrompts(server, ctx);
|
|
102
|
-
return {
|
|
86
|
+
return {
|
|
87
|
+
server,
|
|
88
|
+
ctx,
|
|
89
|
+
registry,
|
|
90
|
+
toolCount,
|
|
91
|
+
close: async () => {
|
|
92
|
+
await ctx.browser?.closeAll();
|
|
93
|
+
await server.close();
|
|
94
|
+
},
|
|
95
|
+
};
|
|
103
96
|
}
|
|
104
97
|
export function registerPrompts(server, ctx) {
|
|
105
98
|
for (const role of Object.keys(ROLE_PROMPTS)) {
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { BrowserSchema } from '@sdods/contracts';
|
|
3
|
+
import { BROWSER_TOOLS } from '../browser/manifest.js';
|
|
4
|
+
import { UNSAFE_DISABLED_HINT, unsafeToolsEnabled } from '../browser/policy.js';
|
|
5
|
+
import { UPSTREAM_SHAPES } from '../browser/shapes.js';
|
|
6
|
+
import { defineTool, summarize } from '../registry/registry.js';
|
|
7
|
+
/**
|
|
8
|
+
* The governed browser surface.
|
|
9
|
+
*
|
|
10
|
+
* Every upstream Playwright MCP tool is wrapped rather than exposed directly, so that a browser an
|
|
11
|
+
* agent drives is bound to a project and environment, carries that project's test-id attribute and
|
|
12
|
+
* login state, writes its artifacts into the run directory, has its auth headers redacted on the
|
|
13
|
+
* way out, and can be refused by scope. The raw server offered all of that ungoverned, with
|
|
14
|
+
* `browser_run_code_unsafe` sitting in the same list as `browser_click`.
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_SESSION = 'default';
|
|
17
|
+
/** Output beyond this is clipped: `summarize()` bounds `data`, never `text`, and one snapshot of a
|
|
18
|
+
* large single-page app is tens of thousands of tokens. Every tool that can produce that much has
|
|
19
|
+
* a `filename` argument, which the clip message points at. */
|
|
20
|
+
const MAX_TEXT = 20_000;
|
|
21
|
+
const sessionArg = {
|
|
22
|
+
sessionId: z
|
|
23
|
+
.string()
|
|
24
|
+
.optional()
|
|
25
|
+
.describe(`Browser session (default: "${DEFAULT_SESSION}"). See browser_session_open.`),
|
|
26
|
+
};
|
|
27
|
+
function clip(text) {
|
|
28
|
+
if (text.length <= MAX_TEXT)
|
|
29
|
+
return text;
|
|
30
|
+
return `${text.slice(0, MAX_TEXT)}\n\n… truncated at ${MAX_TEXT} characters. Re-run with a \`filename\` argument to write the full output into the session directory.`;
|
|
31
|
+
}
|
|
32
|
+
function sessions(ctx) {
|
|
33
|
+
const mgr = ctx.browser;
|
|
34
|
+
if (!mgr)
|
|
35
|
+
throw Object.assign(new Error('The browser capability is disabled on this server.'), {
|
|
36
|
+
error: {
|
|
37
|
+
code: 'CAPABILITY_DISABLED',
|
|
38
|
+
hint: 'Start with --caps browser (it is on by default).',
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
return mgr;
|
|
42
|
+
}
|
|
43
|
+
/** One wrapper per upstream tool. */
|
|
44
|
+
function wrap(name) {
|
|
45
|
+
const spec = BROWSER_TOOLS[name];
|
|
46
|
+
const upstreamShape = UPSTREAM_SHAPES[name];
|
|
47
|
+
return defineTool({
|
|
48
|
+
name,
|
|
49
|
+
title: name.replace(/^browser_/, '').replace(/_/g, ' '),
|
|
50
|
+
description: [
|
|
51
|
+
`Playwright MCP \`${name}\`, bound to an SDODS project and environment.`,
|
|
52
|
+
spec.note,
|
|
53
|
+
spec.pack === 'default' ? undefined : `Requires the \`${spec.pack}\` capability pack.`,
|
|
54
|
+
]
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
.join(' '),
|
|
57
|
+
shape: { ...sessionArg, ...upstreamShape },
|
|
58
|
+
access: spec.access,
|
|
59
|
+
domain: 'browser',
|
|
60
|
+
capability: 'browser',
|
|
61
|
+
...(spec.scope ? { scope: spec.scope } : {}),
|
|
62
|
+
annotations: {
|
|
63
|
+
readOnlyHint: spec.access === 'read',
|
|
64
|
+
destructiveHint: spec.access === 'write',
|
|
65
|
+
openWorldHint: true,
|
|
66
|
+
},
|
|
67
|
+
docsPath: '/docs/reference/mcp-tools',
|
|
68
|
+
handler: async (args, ctx) => {
|
|
69
|
+
if (spec.scope === 'browser:admin' && !unsafeToolsEnabled())
|
|
70
|
+
throw Object.assign(new Error(`${name} is disabled.`), {
|
|
71
|
+
error: { code: 'BROWSER_UNSAFE_DISABLED', hint: UNSAFE_DISABLED_HINT },
|
|
72
|
+
});
|
|
73
|
+
const mgr = sessions(ctx);
|
|
74
|
+
const id = args.sessionId ?? DEFAULT_SESSION;
|
|
75
|
+
// Only a tool that acts may bring a browser up. `browser:read` is granted to every viewer by
|
|
76
|
+
// the READ_SCOPES filter, so letting an observe tool open a session would let a viewer start
|
|
77
|
+
// a browser without ever holding browser:write.
|
|
78
|
+
if (spec.access === 'read' && !mgr.has(id))
|
|
79
|
+
throw Object.assign(new Error(`No browser session "${id}".`), {
|
|
80
|
+
error: {
|
|
81
|
+
code: 'BROWSER_NO_SESSION',
|
|
82
|
+
hint: 'Open one with browser_session_open, or call an action tool first.',
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
if (!ctx.project && !mgr.has(id))
|
|
86
|
+
throw Object.assign(new Error('No project for an implicit browser session.'), {
|
|
87
|
+
error: {
|
|
88
|
+
code: 'BROWSER_NO_PROJECT',
|
|
89
|
+
hint: 'Call browser_session_open with a project, or start the server with --project.',
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
const entry = await mgr.ensure(id, { project: ctx.project, env: ctx.env });
|
|
93
|
+
const result = await entry.driver.call(name, args);
|
|
94
|
+
return {
|
|
95
|
+
text: clip(result.text) || `${name} ok`,
|
|
96
|
+
images: result.images,
|
|
97
|
+
isError: result.isError,
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const sessionTools = [
|
|
103
|
+
defineTool({
|
|
104
|
+
name: 'browser_session_open',
|
|
105
|
+
title: 'Open a browser session',
|
|
106
|
+
description: 'Start a browser bound to a project and environment. Applies the project test-id attribute, the environment locale/timezone/headers, an optional role login state, and writes artifacts into the run directory. Calling an action tool without a session opens a default one, so this is for overrides.',
|
|
107
|
+
shape: {
|
|
108
|
+
sessionId: z.string().optional().describe(`default: "${DEFAULT_SESSION}"`),
|
|
109
|
+
project: z.string().optional().describe('project slug (default: the server’s --project)'),
|
|
110
|
+
env: z.string().optional(),
|
|
111
|
+
role: z
|
|
112
|
+
.string()
|
|
113
|
+
.optional()
|
|
114
|
+
.describe('reuse the login state captured for this @user:<role>; must already exist'),
|
|
115
|
+
browser: BrowserSchema.optional().describe('default: chromium'),
|
|
116
|
+
headed: z.boolean().optional().describe('default: false (headless)'),
|
|
117
|
+
device: z.string().optional().describe('Playwright device profile, e.g. "iPhone 15"'),
|
|
118
|
+
viewport: z.object({ width: z.number().int(), height: z.number().int() }).optional(),
|
|
119
|
+
caps: z
|
|
120
|
+
.array(z.enum(['vision', 'pdf', 'devtools']))
|
|
121
|
+
.optional()
|
|
122
|
+
.describe('extra Playwright capability packs to enable for this session'),
|
|
123
|
+
snapshotMode: z
|
|
124
|
+
.enum(['full', 'none'])
|
|
125
|
+
.optional()
|
|
126
|
+
.describe('"none" keeps accessibility snapshots out of every response'),
|
|
127
|
+
images: z.enum(['allow', 'omit']).optional(),
|
|
128
|
+
},
|
|
129
|
+
access: 'write',
|
|
130
|
+
domain: 'browser',
|
|
131
|
+
capability: 'browser',
|
|
132
|
+
annotations: { destructiveHint: true, openWorldHint: true },
|
|
133
|
+
docsPath: '/docs/reference/mcp-tools',
|
|
134
|
+
handler: async (args, ctx) => {
|
|
135
|
+
const project = args.project ?? ctx.project;
|
|
136
|
+
if (!project)
|
|
137
|
+
throw Object.assign(new Error('A project is required to open a browser session.'), {
|
|
138
|
+
error: { code: 'BROWSER_NO_PROJECT', hint: 'Pass project, or start with --project.' },
|
|
139
|
+
});
|
|
140
|
+
const info = await sessions(ctx).open({
|
|
141
|
+
sessionId: args.sessionId ?? DEFAULT_SESSION,
|
|
142
|
+
project,
|
|
143
|
+
env: args.env ?? ctx.env,
|
|
144
|
+
role: args.role,
|
|
145
|
+
browser: args.browser,
|
|
146
|
+
headed: args.headed,
|
|
147
|
+
device: args.device,
|
|
148
|
+
viewport: args.viewport,
|
|
149
|
+
caps: args.caps,
|
|
150
|
+
snapshotMode: args.snapshotMode,
|
|
151
|
+
images: args.images,
|
|
152
|
+
});
|
|
153
|
+
return { text: summarize(`Browser session ${info.sessionId}`, info), data: info };
|
|
154
|
+
},
|
|
155
|
+
}),
|
|
156
|
+
defineTool({
|
|
157
|
+
name: 'browser_session_close',
|
|
158
|
+
title: 'Close a browser session',
|
|
159
|
+
description: 'End a session and stop its browser. Distinct from browser_close, which closes the page and leaves the session running. Artifacts stay on disk.',
|
|
160
|
+
shape: { sessionId: z.string().optional() },
|
|
161
|
+
access: 'write',
|
|
162
|
+
domain: 'browser',
|
|
163
|
+
capability: 'browser',
|
|
164
|
+
annotations: { destructiveHint: true },
|
|
165
|
+
docsPath: '/docs/reference/mcp-tools',
|
|
166
|
+
handler: async (args, ctx) => {
|
|
167
|
+
const id = args.sessionId ?? DEFAULT_SESSION;
|
|
168
|
+
const closed = await sessions(ctx).close(id);
|
|
169
|
+
return { text: closed ? `Closed browser session ${id}.` : `No browser session ${id}.` };
|
|
170
|
+
},
|
|
171
|
+
}),
|
|
172
|
+
defineTool({
|
|
173
|
+
name: 'browser_session_list',
|
|
174
|
+
title: 'List browser sessions',
|
|
175
|
+
description: 'Open browser sessions on this connection, with their project, env and artifacts.',
|
|
176
|
+
shape: {},
|
|
177
|
+
access: 'read',
|
|
178
|
+
domain: 'browser',
|
|
179
|
+
capability: 'browser',
|
|
180
|
+
docsPath: '/docs/reference/mcp-tools',
|
|
181
|
+
handler: async (_args, ctx) => {
|
|
182
|
+
const list = sessions(ctx).list();
|
|
183
|
+
return { text: summarize('Browser sessions', list), data: list };
|
|
184
|
+
},
|
|
185
|
+
}),
|
|
186
|
+
];
|
|
187
|
+
export const browserTools = [
|
|
188
|
+
...sessionTools,
|
|
189
|
+
...Object.keys(BROWSER_TOOLS).sort().map(wrap),
|
|
190
|
+
];
|
|
191
|
+
//# sourceMappingURL=browser.js.map
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ToolRegistry, type SdodsTool } from '../registry/registry.js';
|
|
2
2
|
import { analyzeTools } from './analyze.js';
|
|
3
|
+
import { browserTools } from './browser.js';
|
|
3
4
|
import { dataTools } from './data.js';
|
|
4
5
|
import { featureTools } from './feature.js';
|
|
5
6
|
import { issueTools, scheduleTools } from './issue-schedule.js';
|
|
@@ -8,5 +9,5 @@ import { proposalTools } from './proposal.js';
|
|
|
8
9
|
import { runTools } from './run.js';
|
|
9
10
|
export declare const ALL_TOOLS: Array<SdodsTool<any>>;
|
|
10
11
|
export declare function createRegistry(extra?: Array<SdodsTool<any>>): ToolRegistry;
|
|
11
|
-
export { analyzeTools, dataTools, featureTools, issueTools, projectTools, proposalTools, runTools, scheduleTools, };
|
|
12
|
+
export { analyzeTools, browserTools, dataTools, featureTools, issueTools, projectTools, proposalTools, runTools, scheduleTools, };
|
|
12
13
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/tools/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ToolRegistry } from '../registry/registry.js';
|
|
2
2
|
import { analyzeTools } from './analyze.js';
|
|
3
|
+
import { browserTools } from './browser.js';
|
|
3
4
|
import { dataTools } from './data.js';
|
|
4
5
|
import { featureTools } from './feature.js';
|
|
5
6
|
import { issueTools, scheduleTools } from './issue-schedule.js';
|
|
@@ -10,6 +11,7 @@ export const ALL_TOOLS = [
|
|
|
10
11
|
...projectTools,
|
|
11
12
|
...featureTools,
|
|
12
13
|
...runTools,
|
|
14
|
+
...browserTools,
|
|
13
15
|
...dataTools,
|
|
14
16
|
...analyzeTools,
|
|
15
17
|
...issueTools,
|
|
@@ -19,5 +21,5 @@ export const ALL_TOOLS = [
|
|
|
19
21
|
export function createRegistry(extra = []) {
|
|
20
22
|
return new ToolRegistry().registerAll(ALL_TOOLS).registerAll(extra);
|
|
21
23
|
}
|
|
22
|
-
export { analyzeTools, dataTools, featureTools, issueTools, projectTools, proposalTools, runTools, scheduleTools, };
|
|
24
|
+
export { analyzeTools, browserTools, dataTools, featureTools, issueTools, projectTools, proposalTools, runTools, scheduleTools, };
|
|
23
25
|
//# sourceMappingURL=index.js.map
|
package/dist/tools/run.d.ts
CHANGED
package/dist/tools/run.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { runFiles } from '@sdods/contracts';
|
|
4
|
+
import { BrowserSchema, runFiles } from '@sdods/contracts';
|
|
5
5
|
import { SdodsCliError, sdodsCli } from '../cli.js';
|
|
6
6
|
import { getRun, lastRun, listRuns, listScenarioDirs, readScenarioDir, screenshotUri, } from '../fs.js';
|
|
7
7
|
import { defineTool, summarize } from '../registry/registry.js';
|
|
@@ -10,9 +10,7 @@ const runArgs = {
|
|
|
10
10
|
env: z.string().optional(),
|
|
11
11
|
tags: z.string().optional().describe('Cucumber tag expression, e.g. "@smoke and not @mock"'),
|
|
12
12
|
layers: z.array(z.enum(['ui', 'api', 'hybrid', 'recorded'])).optional(),
|
|
13
|
-
browsers: z
|
|
14
|
-
.array(z.enum(['chromium', 'firefox', 'webkit', 'mobile-chrome', 'mobile-safari']))
|
|
15
|
-
.optional(),
|
|
13
|
+
browsers: z.array(BrowserSchema).optional(),
|
|
16
14
|
process: z
|
|
17
15
|
.string()
|
|
18
16
|
.optional()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdods/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "SDODS MCP server: project analysis, runs, results and proposals as tools for AI clients.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "SDODS <admin@sdods.com>",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"./proposals": "./dist/proposals.js",
|
|
14
14
|
"./prompts": "./dist/prompts/index.js",
|
|
15
15
|
"./install": "./dist/install/index.js",
|
|
16
|
+
"./browser": "./dist/browser/index.js",
|
|
16
17
|
"./transports/stdio": "./dist/transports/stdio.js",
|
|
17
18
|
"./transports/http": "./dist/transports/http.js"
|
|
18
19
|
},
|
|
@@ -26,9 +27,10 @@
|
|
|
26
27
|
"node": ">=22"
|
|
27
28
|
},
|
|
28
29
|
"dependencies": {
|
|
29
|
-
"@sdods/contracts": "0.
|
|
30
|
+
"@sdods/contracts": "0.5.1",
|
|
30
31
|
"@cucumber/gherkin": "^42.0.1",
|
|
31
32
|
"@cucumber/messages": "^34.2.1",
|
|
33
|
+
"@modelcontextprotocol/client": "^2.0.0",
|
|
32
34
|
"@modelcontextprotocol/node": "^2.0.0",
|
|
33
35
|
"@modelcontextprotocol/server": "^2.0.0",
|
|
34
36
|
"csv-parse": "^7.0.2",
|