@scenar/mcp-server 0.2.0
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/LICENSE +190 -0
- package/README.md +56 -0
- package/bin/scenar-mcp.d.ts +3 -0
- package/bin/scenar-mcp.d.ts.map +1 -0
- package/bin/scenar-mcp.js +7 -0
- package/bin/scenar-mcp.js.map +1 -0
- package/package.json +32 -0
- package/skill/SKILL.md +189 -0
- package/skill/reference.md +153 -0
- package/src/__tests__/server.test.ts +79 -0
- package/src/index.d.ts +8 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.js +20 -0
- package/src/index.js.map +1 -0
- package/src/index.ts +23 -0
- package/src/project.d.ts +11 -0
- package/src/project.d.ts.map +1 -0
- package/src/project.js +24 -0
- package/src/project.js.map +1 -0
- package/src/project.ts +25 -0
- package/src/resources.d.ts +10 -0
- package/src/resources.d.ts.map +1 -0
- package/src/resources.js +81 -0
- package/src/resources.js.map +1 -0
- package/src/resources.ts +126 -0
- package/src/serve-registry.d.ts +8 -0
- package/src/serve-registry.d.ts.map +1 -0
- package/src/serve-registry.js +29 -0
- package/src/serve-registry.js.map +1 -0
- package/src/serve-registry.ts +33 -0
- package/src/server.d.ts +10 -0
- package/src/server.d.ts.map +1 -0
- package/src/server.js +20 -0
- package/src/server.js.map +1 -0
- package/src/server.ts +23 -0
- package/src/tools.d.ts +4 -0
- package/src/tools.d.ts.map +1 -0
- package/src/tools.js +289 -0
- package/src/tools.js.map +1 -0
- package/src/tools.ts +374 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
6
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
7
|
+
import { createScenarServer } from "../server.js";
|
|
8
|
+
|
|
9
|
+
async function connectClient(): Promise<Client> {
|
|
10
|
+
const server = createScenarServer();
|
|
11
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
12
|
+
const client = new Client({ name: "test-client", version: "0.0.0" });
|
|
13
|
+
await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
|
|
14
|
+
return client;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("Scenar MCP server", () => {
|
|
18
|
+
it("exposes all expected tools", async () => {
|
|
19
|
+
const client = await connectClient();
|
|
20
|
+
const { tools } = await client.listTools();
|
|
21
|
+
const names = tools.map((t) => t.name).sort();
|
|
22
|
+
expect(names).toEqual(
|
|
23
|
+
[
|
|
24
|
+
"scenar_narrate",
|
|
25
|
+
"scenar_pack",
|
|
26
|
+
"scenar_preview_init",
|
|
27
|
+
"scenar_preview_sync",
|
|
28
|
+
"scenar_publish",
|
|
29
|
+
"scenar_render",
|
|
30
|
+
"scenar_serve",
|
|
31
|
+
"scenar_stop_serve",
|
|
32
|
+
"scenar_validate",
|
|
33
|
+
].sort(),
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("exposes the registry resources and scenario templates", async () => {
|
|
38
|
+
const client = await connectClient();
|
|
39
|
+
const { resources } = await client.listResources();
|
|
40
|
+
const uris = resources.map((r) => r.uri);
|
|
41
|
+
expect(uris).toContain("scenar://registry/views");
|
|
42
|
+
expect(uris).toContain("scenar://registry/report");
|
|
43
|
+
expect(uris).toContain("scenar://registry/providers");
|
|
44
|
+
|
|
45
|
+
const { resourceTemplates } = await client.listResourceTemplates();
|
|
46
|
+
const templates = resourceTemplates.map((t) => t.uriTemplate);
|
|
47
|
+
expect(templates).toContain("scenar://scenario/{name}/steps");
|
|
48
|
+
expect(templates).toContain("scenar://scenario/{name}/manifest");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("validates a scenario via the scenar_validate tool", async () => {
|
|
52
|
+
const dir = await mkdtemp(join(tmpdir(), "scenar-mcp-"));
|
|
53
|
+
try {
|
|
54
|
+
const file = join(dir, "bad.yaml");
|
|
55
|
+
// Missing required fields → the validator should report errors.
|
|
56
|
+
await writeFile(file, "kind: Scenario\n", "utf-8");
|
|
57
|
+
|
|
58
|
+
const client = await connectClient();
|
|
59
|
+
const result = (await client.callTool({
|
|
60
|
+
name: "scenar_validate",
|
|
61
|
+
arguments: { file },
|
|
62
|
+
})) as { content: Array<{ type: string; text: string }> };
|
|
63
|
+
|
|
64
|
+
const body = result.content.map((c) => c.text).join("\n");
|
|
65
|
+
expect(body).toContain("Invalid");
|
|
66
|
+
} finally {
|
|
67
|
+
await rm(dir, { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("reads a registry resource, returning guidance when absent", async () => {
|
|
72
|
+
const client = await connectClient();
|
|
73
|
+
const result = await client.readResource({ uri: "scenar://registry/views" });
|
|
74
|
+
expect(result.contents).toHaveLength(1);
|
|
75
|
+
// With no .scenar/ in the test cwd, the body should guide the user.
|
|
76
|
+
const text = String(result.contents[0]!.text);
|
|
77
|
+
expect(text.length).toBeGreaterThan(0);
|
|
78
|
+
});
|
|
79
|
+
});
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { createScenarServer, SERVER_VERSION } from "./server.js";
|
|
2
|
+
/**
|
|
3
|
+
* Start the Scenar MCP server over stdio (how Cursor and other editors launch
|
|
4
|
+
* it). Stays alive until the transport closes, then stops any preview servers
|
|
5
|
+
* started during the session.
|
|
6
|
+
*/
|
|
7
|
+
export declare function main(): Promise<void>;
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAEjE;;;;GAIG;AACH,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAW1C"}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { createScenarServer } from "./server.js";
|
|
3
|
+
import { stopAllServers } from "./serve-registry.js";
|
|
4
|
+
export { createScenarServer, SERVER_VERSION } from "./server.js";
|
|
5
|
+
/**
|
|
6
|
+
* Start the Scenar MCP server over stdio (how Cursor and other editors launch
|
|
7
|
+
* it). Stays alive until the transport closes, then stops any preview servers
|
|
8
|
+
* started during the session.
|
|
9
|
+
*/
|
|
10
|
+
export async function main() {
|
|
11
|
+
const server = createScenarServer();
|
|
12
|
+
const transport = new StdioServerTransport();
|
|
13
|
+
const shutdown = () => {
|
|
14
|
+
void stopAllServers().finally(() => process.exit(0));
|
|
15
|
+
};
|
|
16
|
+
process.on("SIGINT", shutdown);
|
|
17
|
+
process.on("SIGTERM", shutdown);
|
|
18
|
+
await server.connect(transport);
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
package/src/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAEjE;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,IAAI;IACxB,MAAM,MAAM,GAAG,kBAAkB,EAAE,CAAC;IACpC,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAE7C,MAAM,QAAQ,GAAG,GAAG,EAAE;QACpB,KAAK,cAAc,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IACvD,CAAC,CAAC;IACF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAEhC,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC"}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { createScenarServer } from "./server.js";
|
|
3
|
+
import { stopAllServers } from "./serve-registry.js";
|
|
4
|
+
|
|
5
|
+
export { createScenarServer, SERVER_VERSION } from "./server.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Start the Scenar MCP server over stdio (how Cursor and other editors launch
|
|
9
|
+
* it). Stays alive until the transport closes, then stops any preview servers
|
|
10
|
+
* started during the session.
|
|
11
|
+
*/
|
|
12
|
+
export async function main(): Promise<void> {
|
|
13
|
+
const server = createScenarServer();
|
|
14
|
+
const transport = new StdioServerTransport();
|
|
15
|
+
|
|
16
|
+
const shutdown = () => {
|
|
17
|
+
void stopAllServers().finally(() => process.exit(0));
|
|
18
|
+
};
|
|
19
|
+
process.on("SIGINT", shutdown);
|
|
20
|
+
process.on("SIGTERM", shutdown);
|
|
21
|
+
|
|
22
|
+
await server.connect(transport);
|
|
23
|
+
}
|
package/src/project.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The project root the server reads relative paths against. Cursor launches the
|
|
3
|
+
* MCP server with its working directory set to the workspace, but an explicit
|
|
4
|
+
* SCENAR_PROJECT_ROOT always wins (useful for monorepos or non-cwd launches).
|
|
5
|
+
*/
|
|
6
|
+
export declare function projectRoot(): string;
|
|
7
|
+
/** Resolve a possibly-relative path against the project root. */
|
|
8
|
+
export declare function resolveInProject(path: string): string;
|
|
9
|
+
/** Read a project-relative text file, or null if it is missing/unreadable. */
|
|
10
|
+
export declare function readProjectFile(relativePath: string): Promise<string | null>;
|
|
11
|
+
//# sourceMappingURL=project.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../src/project.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,iEAAiE;AACjE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,8EAA8E;AAC9E,wBAAsB,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAMlF"}
|
package/src/project.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { resolve, isAbsolute, join } from "node:path";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
/**
|
|
4
|
+
* The project root the server reads relative paths against. Cursor launches the
|
|
5
|
+
* MCP server with its working directory set to the workspace, but an explicit
|
|
6
|
+
* SCENAR_PROJECT_ROOT always wins (useful for monorepos or non-cwd launches).
|
|
7
|
+
*/
|
|
8
|
+
export function projectRoot() {
|
|
9
|
+
return resolve(process.env.SCENAR_PROJECT_ROOT ?? process.cwd());
|
|
10
|
+
}
|
|
11
|
+
/** Resolve a possibly-relative path against the project root. */
|
|
12
|
+
export function resolveInProject(path) {
|
|
13
|
+
return isAbsolute(path) ? path : join(projectRoot(), path);
|
|
14
|
+
}
|
|
15
|
+
/** Read a project-relative text file, or null if it is missing/unreadable. */
|
|
16
|
+
export async function readProjectFile(relativePath) {
|
|
17
|
+
try {
|
|
18
|
+
return await readFile(join(projectRoot(), relativePath), "utf-8");
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=project.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project.js","sourceRoot":"","sources":["../../src/project.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C;;;;GAIG;AACH,MAAM,UAAU,WAAW;IACzB,OAAO,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;AACnE,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,IAAI,CAAC,CAAC;AAC7D,CAAC;AAED,8EAA8E;AAC9E,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,YAAoB;IACxD,IAAI,CAAC;QACH,OAAO,MAAM,QAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,YAAY,CAAC,EAAE,OAAO,CAAC,CAAC;IACpE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
package/src/project.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { resolve, isAbsolute, join } from "node:path";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The project root the server reads relative paths against. Cursor launches the
|
|
6
|
+
* MCP server with its working directory set to the workspace, but an explicit
|
|
7
|
+
* SCENAR_PROJECT_ROOT always wins (useful for monorepos or non-cwd launches).
|
|
8
|
+
*/
|
|
9
|
+
export function projectRoot(): string {
|
|
10
|
+
return resolve(process.env.SCENAR_PROJECT_ROOT ?? process.cwd());
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Resolve a possibly-relative path against the project root. */
|
|
14
|
+
export function resolveInProject(path: string): string {
|
|
15
|
+
return isAbsolute(path) ? path : join(projectRoot(), path);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Read a project-relative text file, or null if it is missing/unreadable. */
|
|
19
|
+
export async function readProjectFile(relativePath: string): Promise<string | null> {
|
|
20
|
+
try {
|
|
21
|
+
return await readFile(join(projectRoot(), relativePath), "utf-8");
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
/**
|
|
3
|
+
* Register read-only resources the AI can inspect for authoring context: the
|
|
4
|
+
* generated view registry + scan report (in .scenar/), and per-scenario steps +
|
|
5
|
+
* pack manifest. All are read relative to the project root; a missing file
|
|
6
|
+
* returns a short explanatory body rather than an error so the client degrades
|
|
7
|
+
* gracefully.
|
|
8
|
+
*/
|
|
9
|
+
export declare function registerResources(server: McpServer): void;
|
|
10
|
+
//# sourceMappingURL=resources.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resources.d.ts","sourceRoot":"","sources":["../../src/resources.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAoB,MAAM,yCAAyC,CAAC;AAGtF;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA+EzD"}
|
package/src/resources.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { projectRoot, readProjectFile } from "./project.js";
|
|
5
|
+
/**
|
|
6
|
+
* Register read-only resources the AI can inspect for authoring context: the
|
|
7
|
+
* generated view registry + scan report (in .scenar/), and per-scenario steps +
|
|
8
|
+
* pack manifest. All are read relative to the project root; a missing file
|
|
9
|
+
* returns a short explanatory body rather than an error so the client degrades
|
|
10
|
+
* gracefully.
|
|
11
|
+
*/
|
|
12
|
+
export function registerResources(server) {
|
|
13
|
+
registerRegistryFile(server, "scenar-views", "scenar://registry/views", "Scenar view registry", "The generated .scenar/views.ts — the components available to scenarios.", ".scenar/views.ts", "text/plain");
|
|
14
|
+
registerRegistryFile(server, "scenar-report", "scenar://registry/report", "Scenar scan report", "The .scenar/report.md — what the scanner discovered, skipped, and why.", ".scenar/report.md", "text/markdown");
|
|
15
|
+
registerRegistryFile(server, "scenar-providers", "scenar://registry/providers", "Scenar provider wiring", "The .scenar/providers.tsx — how real components are wrapped for preview.", ".scenar/providers.tsx", "text/plain");
|
|
16
|
+
// Per-scenario resources: {name} is a scenario directory under the project.
|
|
17
|
+
server.registerResource("scenar-scenario-steps", new ResourceTemplate("scenar://scenario/{name}/steps", { list: undefined }), {
|
|
18
|
+
title: "Scenario steps",
|
|
19
|
+
description: "The steps.ts of a named scenario directory under the project root.",
|
|
20
|
+
}, async (uri, variables) => {
|
|
21
|
+
const name = String(variables.name);
|
|
22
|
+
const body = await readFirstExisting([
|
|
23
|
+
join(projectRoot(), name, "steps.ts"),
|
|
24
|
+
join(projectRoot(), name, "steps.tsx"),
|
|
25
|
+
]);
|
|
26
|
+
return {
|
|
27
|
+
contents: [
|
|
28
|
+
{
|
|
29
|
+
uri: uri.href,
|
|
30
|
+
mimeType: "text/plain",
|
|
31
|
+
text: body ?? `No steps.ts found for scenario "${name}" under ${projectRoot()}.`,
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
server.registerResource("scenar-scenario-manifest", new ResourceTemplate("scenar://scenario/{name}/manifest", { list: undefined }), {
|
|
37
|
+
title: "Scenario pack manifest",
|
|
38
|
+
description: "The pack-manifest.json of a named scenario's packed bundle (<name>-bundle/).",
|
|
39
|
+
}, async (uri, variables) => {
|
|
40
|
+
const name = String(variables.name);
|
|
41
|
+
const body = await readFirstExisting([
|
|
42
|
+
join(projectRoot(), `${name}-bundle`, "pack-manifest.json"),
|
|
43
|
+
join(projectRoot(), name, "pack-manifest.json"),
|
|
44
|
+
]);
|
|
45
|
+
return {
|
|
46
|
+
contents: [
|
|
47
|
+
{
|
|
48
|
+
uri: uri.href,
|
|
49
|
+
mimeType: "application/json",
|
|
50
|
+
text: body ?? `No pack-manifest.json found for "${name}". Run scenar_pack first.`,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function registerRegistryFile(server, id, uri, title, description, relativePath, mimeType) {
|
|
57
|
+
server.registerResource(id, uri, { title, description, mimeType }, async (u) => {
|
|
58
|
+
const body = await readProjectFile(relativePath);
|
|
59
|
+
return {
|
|
60
|
+
contents: [
|
|
61
|
+
{
|
|
62
|
+
uri: u.href,
|
|
63
|
+
mimeType,
|
|
64
|
+
text: body ?? `No ${relativePath} yet. Run scenar_preview_init to generate it.`,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
async function readFirstExisting(paths) {
|
|
71
|
+
for (const path of paths) {
|
|
72
|
+
try {
|
|
73
|
+
return await readFile(path, "utf-8");
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// try next
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=resources.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resources.js","sourceRoot":"","sources":["../../src/resources.ts"],"names":[],"mappings":"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"}
|
package/src/resources.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { projectRoot, readProjectFile } from "./project.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Register read-only resources the AI can inspect for authoring context: the
|
|
8
|
+
* generated view registry + scan report (in .scenar/), and per-scenario steps +
|
|
9
|
+
* pack manifest. All are read relative to the project root; a missing file
|
|
10
|
+
* returns a short explanatory body rather than an error so the client degrades
|
|
11
|
+
* gracefully.
|
|
12
|
+
*/
|
|
13
|
+
export function registerResources(server: McpServer): void {
|
|
14
|
+
registerRegistryFile(
|
|
15
|
+
server,
|
|
16
|
+
"scenar-views",
|
|
17
|
+
"scenar://registry/views",
|
|
18
|
+
"Scenar view registry",
|
|
19
|
+
"The generated .scenar/views.ts — the components available to scenarios.",
|
|
20
|
+
".scenar/views.ts",
|
|
21
|
+
"text/plain",
|
|
22
|
+
);
|
|
23
|
+
registerRegistryFile(
|
|
24
|
+
server,
|
|
25
|
+
"scenar-report",
|
|
26
|
+
"scenar://registry/report",
|
|
27
|
+
"Scenar scan report",
|
|
28
|
+
"The .scenar/report.md — what the scanner discovered, skipped, and why.",
|
|
29
|
+
".scenar/report.md",
|
|
30
|
+
"text/markdown",
|
|
31
|
+
);
|
|
32
|
+
registerRegistryFile(
|
|
33
|
+
server,
|
|
34
|
+
"scenar-providers",
|
|
35
|
+
"scenar://registry/providers",
|
|
36
|
+
"Scenar provider wiring",
|
|
37
|
+
"The .scenar/providers.tsx — how real components are wrapped for preview.",
|
|
38
|
+
".scenar/providers.tsx",
|
|
39
|
+
"text/plain",
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Per-scenario resources: {name} is a scenario directory under the project.
|
|
43
|
+
server.registerResource(
|
|
44
|
+
"scenar-scenario-steps",
|
|
45
|
+
new ResourceTemplate("scenar://scenario/{name}/steps", { list: undefined }),
|
|
46
|
+
{
|
|
47
|
+
title: "Scenario steps",
|
|
48
|
+
description: "The steps.ts of a named scenario directory under the project root.",
|
|
49
|
+
},
|
|
50
|
+
async (uri, variables) => {
|
|
51
|
+
const name = String(variables.name);
|
|
52
|
+
const body = await readFirstExisting([
|
|
53
|
+
join(projectRoot(), name, "steps.ts"),
|
|
54
|
+
join(projectRoot(), name, "steps.tsx"),
|
|
55
|
+
]);
|
|
56
|
+
return {
|
|
57
|
+
contents: [
|
|
58
|
+
{
|
|
59
|
+
uri: uri.href,
|
|
60
|
+
mimeType: "text/plain",
|
|
61
|
+
text: body ?? `No steps.ts found for scenario "${name}" under ${projectRoot()}.`,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
server.registerResource(
|
|
69
|
+
"scenar-scenario-manifest",
|
|
70
|
+
new ResourceTemplate("scenar://scenario/{name}/manifest", { list: undefined }),
|
|
71
|
+
{
|
|
72
|
+
title: "Scenario pack manifest",
|
|
73
|
+
description: "The pack-manifest.json of a named scenario's packed bundle (<name>-bundle/).",
|
|
74
|
+
},
|
|
75
|
+
async (uri, variables) => {
|
|
76
|
+
const name = String(variables.name);
|
|
77
|
+
const body = await readFirstExisting([
|
|
78
|
+
join(projectRoot(), `${name}-bundle`, "pack-manifest.json"),
|
|
79
|
+
join(projectRoot(), name, "pack-manifest.json"),
|
|
80
|
+
]);
|
|
81
|
+
return {
|
|
82
|
+
contents: [
|
|
83
|
+
{
|
|
84
|
+
uri: uri.href,
|
|
85
|
+
mimeType: "application/json",
|
|
86
|
+
text: body ?? `No pack-manifest.json found for "${name}". Run scenar_pack first.`,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function registerRegistryFile(
|
|
95
|
+
server: McpServer,
|
|
96
|
+
id: string,
|
|
97
|
+
uri: string,
|
|
98
|
+
title: string,
|
|
99
|
+
description: string,
|
|
100
|
+
relativePath: string,
|
|
101
|
+
mimeType: string,
|
|
102
|
+
): void {
|
|
103
|
+
server.registerResource(id, uri, { title, description, mimeType }, async (u) => {
|
|
104
|
+
const body = await readProjectFile(relativePath);
|
|
105
|
+
return {
|
|
106
|
+
contents: [
|
|
107
|
+
{
|
|
108
|
+
uri: u.href,
|
|
109
|
+
mimeType,
|
|
110
|
+
text: body ?? `No ${relativePath} yet. Run scenar_preview_init to generate it.`,
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function readFirstExisting(paths: string[]): Promise<string | null> {
|
|
118
|
+
for (const path of paths) {
|
|
119
|
+
try {
|
|
120
|
+
return await readFile(path, "utf-8");
|
|
121
|
+
} catch {
|
|
122
|
+
// try next
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BundleServerHandle } from "@scenar/cli/api";
|
|
2
|
+
export declare function rememberServer(handle: BundleServerHandle): void;
|
|
3
|
+
export declare function listServers(): string[];
|
|
4
|
+
/** Stop a server by URL. Returns true if one was running and is now stopped. */
|
|
5
|
+
export declare function stopServer(url: string): Promise<boolean>;
|
|
6
|
+
/** Stop every running server (called on process shutdown). */
|
|
7
|
+
export declare function stopAllServers(): Promise<void>;
|
|
8
|
+
//# sourceMappingURL=serve-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serve-registry.d.ts","sourceRoot":"","sources":["../../src/serve-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAU1D,wBAAgB,cAAc,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAE/D;AAED,wBAAgB,WAAW,IAAI,MAAM,EAAE,CAEtC;AAED,gFAAgF;AAChF,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAM9D;AAED,8DAA8D;AAC9D,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAIpD"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks preview servers started by the `scenar_serve` tool so they outlive a
|
|
3
|
+
* single tool call (the AI starts one, then keeps working) and can be listed or
|
|
4
|
+
* stopped. Keyed by the served URL. The server process owns these; they are all
|
|
5
|
+
* closed on shutdown.
|
|
6
|
+
*/
|
|
7
|
+
const servers = new Map();
|
|
8
|
+
export function rememberServer(handle) {
|
|
9
|
+
servers.set(handle.url, handle);
|
|
10
|
+
}
|
|
11
|
+
export function listServers() {
|
|
12
|
+
return [...servers.keys()];
|
|
13
|
+
}
|
|
14
|
+
/** Stop a server by URL. Returns true if one was running and is now stopped. */
|
|
15
|
+
export async function stopServer(url) {
|
|
16
|
+
const handle = servers.get(url);
|
|
17
|
+
if (!handle)
|
|
18
|
+
return false;
|
|
19
|
+
await handle.close();
|
|
20
|
+
servers.delete(url);
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
/** Stop every running server (called on process shutdown). */
|
|
24
|
+
export async function stopAllServers() {
|
|
25
|
+
const handles = [...servers.values()];
|
|
26
|
+
servers.clear();
|
|
27
|
+
await Promise.all(handles.map((h) => h.close().catch(() => { })));
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=serve-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serve-registry.js","sourceRoot":"","sources":["../../src/serve-registry.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,OAAO,GAAG,IAAI,GAAG,EAA8B,CAAC;AAEtD,MAAM,UAAU,cAAc,CAAC,MAA0B;IACvD,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;AAC7B,CAAC;AAED,gFAAgF;AAChF,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,GAAW;IAC1C,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAChC,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC1B,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IACrB,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACpB,OAAO,IAAI,CAAC;AACd,CAAC;AAED,8DAA8D;AAC9D,MAAM,CAAC,KAAK,UAAU,cAAc;IAClC,MAAM,OAAO,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACtC,OAAO,CAAC,KAAK,EAAE,CAAC;IAChB,MAAM,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AACnE,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { BundleServerHandle } from "@scenar/cli/api";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tracks preview servers started by the `scenar_serve` tool so they outlive a
|
|
5
|
+
* single tool call (the AI starts one, then keeps working) and can be listed or
|
|
6
|
+
* stopped. Keyed by the served URL. The server process owns these; they are all
|
|
7
|
+
* closed on shutdown.
|
|
8
|
+
*/
|
|
9
|
+
const servers = new Map<string, BundleServerHandle>();
|
|
10
|
+
|
|
11
|
+
export function rememberServer(handle: BundleServerHandle): void {
|
|
12
|
+
servers.set(handle.url, handle);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function listServers(): string[] {
|
|
16
|
+
return [...servers.keys()];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Stop a server by URL. Returns true if one was running and is now stopped. */
|
|
20
|
+
export async function stopServer(url: string): Promise<boolean> {
|
|
21
|
+
const handle = servers.get(url);
|
|
22
|
+
if (!handle) return false;
|
|
23
|
+
await handle.close();
|
|
24
|
+
servers.delete(url);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Stop every running server (called on process shutdown). */
|
|
29
|
+
export async function stopAllServers(): Promise<void> {
|
|
30
|
+
const handles = [...servers.values()];
|
|
31
|
+
servers.clear();
|
|
32
|
+
await Promise.all(handles.map((h) => h.close().catch(() => {})));
|
|
33
|
+
}
|
package/src/server.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
/** The server's advertised version (kept in step with the package). */
|
|
3
|
+
export declare const SERVER_VERSION = "0.0.1";
|
|
4
|
+
/**
|
|
5
|
+
* Build a fully-configured Scenar MCP server (tools + resources registered),
|
|
6
|
+
* ready to connect to a transport. Factored out so tests can construct it
|
|
7
|
+
* without standing up stdio.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createScenarServer(): McpServer;
|
|
10
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIpE,uEAAuE;AACvE,eAAO,MAAM,cAAc,UAAU,CAAC;AAEtC;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,SAAS,CAU9C"}
|
package/src/server.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { registerTools } from "./tools.js";
|
|
3
|
+
import { registerResources } from "./resources.js";
|
|
4
|
+
/** The server's advertised version (kept in step with the package). */
|
|
5
|
+
export const SERVER_VERSION = "0.0.1";
|
|
6
|
+
/**
|
|
7
|
+
* Build a fully-configured Scenar MCP server (tools + resources registered),
|
|
8
|
+
* ready to connect to a transport. Factored out so tests can construct it
|
|
9
|
+
* without standing up stdio.
|
|
10
|
+
*/
|
|
11
|
+
export function createScenarServer() {
|
|
12
|
+
const server = new McpServer({
|
|
13
|
+
name: "scenar",
|
|
14
|
+
version: SERVER_VERSION,
|
|
15
|
+
});
|
|
16
|
+
registerTools(server);
|
|
17
|
+
registerResources(server);
|
|
18
|
+
return server;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAEnD,uEAAuE;AACvE,MAAM,CAAC,MAAM,cAAc,GAAG,OAAO,CAAC;AAEtC;;;;GAIG;AACH,MAAM,UAAU,kBAAkB;IAChC,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,QAAQ;QACd,OAAO,EAAE,cAAc;KACxB,CAAC,CAAC;IAEH,aAAa,CAAC,MAAM,CAAC,CAAC;IACtB,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAE1B,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { registerTools } from "./tools.js";
|
|
3
|
+
import { registerResources } from "./resources.js";
|
|
4
|
+
|
|
5
|
+
/** The server's advertised version (kept in step with the package). */
|
|
6
|
+
export const SERVER_VERSION = "0.0.1";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build a fully-configured Scenar MCP server (tools + resources registered),
|
|
10
|
+
* ready to connect to a transport. Factored out so tests can construct it
|
|
11
|
+
* without standing up stdio.
|
|
12
|
+
*/
|
|
13
|
+
export function createScenarServer(): McpServer {
|
|
14
|
+
const server = new McpServer({
|
|
15
|
+
name: "scenar",
|
|
16
|
+
version: SERVER_VERSION,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
registerTools(server);
|
|
20
|
+
registerResources(server);
|
|
21
|
+
|
|
22
|
+
return server;
|
|
23
|
+
}
|
package/src/tools.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/tools.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAsCzE,gDAAgD;AAChD,wBAAgB,aAAa,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAUrD"}
|