diff --git a/.changeset/shaggy-squids-clap.md b/.changeset/shaggy-squids-clap.md new file mode 100644 index 0000000..341e88d --- /dev/null +++ b/.changeset/shaggy-squids-clap.md @@ -0,0 +1,5 @@ +--- +"@webiny/stdlib": patch +--- + +feat: add MCP server for AI agent skill discovery (`stdlib-mcp serve` / `stdlib-mcp configure`) diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 1e44133..3a260eb 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -13,8 +13,8 @@ jobs: name: Dependency audit runs-on: ubuntu-latest steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version: 24 cache: yarn @@ -29,5 +29,5 @@ jobs: contents: read pull-requests: write steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49b0878..84fce7b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,8 +13,8 @@ jobs: name: Lockfile immutability runs-on: ubuntu-latest steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version: 24 cache: yarn @@ -24,7 +24,7 @@ jobs: name: No committed dist runs-on: ubuntu-latest steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Verify dist is not tracked in git run: | tracked=$(git ls-files dist/) @@ -40,8 +40,8 @@ jobs: name: Format runs-on: ubuntu-latest steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version: 24 cache: yarn @@ -52,8 +52,8 @@ jobs: name: Lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version: 24 cache: yarn @@ -64,8 +64,8 @@ jobs: name: Import check (adio) runs-on: ubuntu-latest steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version: 24 cache: yarn @@ -76,8 +76,8 @@ jobs: name: Typecheck runs-on: ubuntu-latest steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version: 24 cache: yarn @@ -89,8 +89,8 @@ jobs: needs: [lockfile, format, lint, imports, typecheck] runs-on: ubuntu-latest steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version: 24 cache: yarn @@ -102,8 +102,8 @@ jobs: needs: [build] runs-on: ubuntu-latest steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version: 24 cache: yarn @@ -115,8 +115,8 @@ jobs: needs: [build] runs-on: ubuntu-latest steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version: 24 cache: yarn diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index c462000..2354f21 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -12,7 +12,7 @@ jobs: name: Conventional commit title runs-on: ubuntu-latest steps: - - uses: amannn/action-semantic-pull-request@e32d7e603df1aa1ba07e981f2a23455dee596825 # v5 + - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4eb5f0e..1ec7016 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,8 +15,8 @@ jobs: if: ${{ github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version: 24 cache: yarn diff --git a/.gitignore b/.gitignore index 788355f..6fa2e1a 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ lerna.json .stormTests ./nextjs +.superpowers /.local CLAUDE.local.md .claude/*.local.* diff --git a/__tests__/mcp/Configure.test.ts b/__tests__/mcp/Configure.test.ts new file mode 100644 index 0000000..54db7fe --- /dev/null +++ b/__tests__/mcp/Configure.test.ts @@ -0,0 +1,196 @@ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { + writeMcpConfig, + writeHintFile, + stdlibHintBlock +} from "../../src/mcp/features/Configure/agents/shared.js"; + +describe("writeMcpConfig", () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = join(tmpdir(), `wby-mcp-config-test-${Date.now()}`); + mkdirSync(tmpDir, { recursive: true }); + }); + + afterEach(() => { + rmSync(tmpDir, { recursive: true, force: true }); + }); + + it("should create new config file with stdlib entry", () => { + writeMcpConfig({ cwd: tmpDir, configFile: ".mcp.json", configKey: "mcpServers" }); + + const content = JSON.parse(readFileSync(join(tmpDir, ".mcp.json"), "utf-8")); + expect(content.mcpServers.stdlib).toBeDefined(); + expect(content.mcpServers.stdlib.command).toBe("npx"); + expect(content.mcpServers.stdlib.args).toContain("stdlib-mcp"); + }); + + it("should preserve existing entries when patching", () => { + const configPath = join(tmpDir, ".mcp.json"); + writeFileSync( + configPath, + JSON.stringify({ + mcpServers: { other: { command: "other-cmd" } } + }) + ); + + writeMcpConfig({ cwd: tmpDir, configFile: ".mcp.json", configKey: "mcpServers" }); + + const content = JSON.parse(readFileSync(configPath, "utf-8")); + expect(content.mcpServers.other).toBeDefined(); + expect(content.mcpServers.stdlib).toBeDefined(); + }); + + it("should skip if stdlib entry already exists", () => { + const configPath = join(tmpDir, ".mcp.json"); + const original = { mcpServers: { stdlib: { command: "custom" } } }; + writeFileSync(configPath, JSON.stringify(original)); + + writeMcpConfig({ cwd: tmpDir, configFile: ".mcp.json", configKey: "mcpServers" }); + + const content = JSON.parse(readFileSync(configPath, "utf-8")); + expect(content.mcpServers.stdlib.command).toBe("custom"); + }); + + it("should create nested directories", () => { + writeMcpConfig({ + cwd: tmpDir, + configFile: ".cursor/mcp.json", + configKey: "mcpServers" + }); + + expect(existsSync(join(tmpDir, ".cursor", "mcp.json"))).toBe(true); + }); + + it("should use servers key for copilot", () => { + writeMcpConfig({ cwd: tmpDir, configFile: ".vscode/mcp.json", configKey: "servers" }); + + const content = JSON.parse(readFileSync(join(tmpDir, ".vscode", "mcp.json"), "utf-8")); + expect(content.servers.stdlib).toBeDefined(); + }); +}); + +describe("writeHintFile", () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = join(tmpdir(), `wby-mcp-hint-test-${Date.now()}`); + mkdirSync(tmpDir, { recursive: true }); + }); + + afterEach(() => { + rmSync(tmpDir, { recursive: true, force: true }); + }); + + it("should create new hint file", () => { + writeHintFile({ + cwd: tmpDir, + hintFile: "CLAUDE.md", + content: stdlibHintBlock() + }); + + const text = readFileSync(join(tmpDir, "CLAUDE.md"), "utf-8"); + expect(text).toContain("list_stdlib_skills"); + }); + + it("should append to existing file with blank line separator", () => { + const hintPath = join(tmpDir, "CLAUDE.md"); + writeFileSync(hintPath, "# Existing content"); + + writeHintFile({ + cwd: tmpDir, + hintFile: "CLAUDE.md", + content: stdlibHintBlock() + }); + + const text = readFileSync(hintPath, "utf-8"); + expect(text).toContain("# Existing content"); + expect(text).toContain("list_stdlib_skills"); + expect(text).toContain("\n\n## @webiny/stdlib MCP"); + }); + + it("should skip if marker already present", () => { + const hintPath = join(tmpDir, "CLAUDE.md"); + writeFileSync(hintPath, "Already has list_stdlib_skills marker."); + + writeHintFile({ + cwd: tmpDir, + hintFile: "CLAUDE.md", + content: stdlibHintBlock() + }); + + const text = readFileSync(hintPath, "utf-8"); + expect(text).toBe("Already has list_stdlib_skills marker."); + }); + + it("should create nested directories", () => { + writeHintFile({ + cwd: tmpDir, + hintFile: ".cursor/rules/stdlib.mdc", + content: stdlibHintBlock() + }); + + expect(existsSync(join(tmpDir, ".cursor", "rules", "stdlib.mdc"))).toBe(true); + }); +}); + +describe("agent adapters", () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = join(tmpdir(), `wby-mcp-agent-test-${Date.now()}`); + mkdirSync(tmpDir, { recursive: true }); + }); + + afterEach(() => { + rmSync(tmpDir, { recursive: true, force: true }); + }); + + const agents = [ + { module: "claude", configFile: ".mcp.json", hintFile: "CLAUDE.md" }, + { module: "cursor", configFile: ".cursor/mcp.json", hintFile: ".cursor/rules/stdlib.mdc" }, + { + module: "copilot", + configFile: ".vscode/mcp.json", + hintFile: ".github/copilot-instructions.md" + }, + { + module: "windsurf", + configFile: ".windsurf/mcp.json", + hintFile: ".windsurf/rules/stdlib.md" + }, + { module: "kiro", configFile: ".kiro/settings/mcp.json", hintFile: "AGENTS.md" }, + { module: "opencode", configFile: "opencode.json", hintFile: "AGENTS.md" } + ]; + + for (const { module: mod, configFile, hintFile } of agents) { + it(`should configure ${mod} agent`, async () => { + const agent = await import( + /* @vite-ignore */ `../../src/mcp/features/Configure/agents/${mod}.js` + ); + await agent.default.init({ cwd: tmpDir }); + + expect(existsSync(join(tmpDir, configFile))).toBe(true); + expect(existsSync(join(tmpDir, hintFile))).toBe(true); + + const config = JSON.parse(readFileSync(join(tmpDir, configFile), "utf-8")); + const hint = readFileSync(join(tmpDir, hintFile), "utf-8"); + + expect(JSON.stringify(config)).toContain("stdlib"); + expect(hint).toContain("list_stdlib_skills"); + }); + } + + it("should configure cline agent (no hint file)", async () => { + const agent = await import( + /* @vite-ignore */ "../../src/mcp/features/Configure/agents/cline.js" + ); + await agent.default.init({ cwd: tmpDir }); + + expect(existsSync(join(tmpDir, ".vscode/cline_mcp_settings.json"))).toBe(true); + }); +}); diff --git a/__tests__/mcp/McpServer.test.ts b/__tests__/mcp/McpServer.test.ts new file mode 100644 index 0000000..3f5f1f8 --- /dev/null +++ b/__tests__/mcp/McpServer.test.ts @@ -0,0 +1,142 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { Container } from "@webiny/di"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { SkillDiscoveryConfig, McpServer, McpServerFeature } from "../../src/mcp/index.js"; +import { mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { afterEach } from "vitest"; + +describe("McpServer", () => { + let tmpDir: string; + let client: Client; + let cleanup: () => Promise; + + async function startServer(scanPaths: string[]): Promise { + const container = new Container(); + container.registerInstance(SkillDiscoveryConfig, { scanPaths }); + McpServerFeature.register(container); + const server = container.resolve(McpServer); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + const c = new Client({ name: "test-client", version: "1.0.0" }); + await server.startWithTransport(serverTransport); + await c.connect(clientTransport); + + cleanup = async () => { + await c.close(); + }; + + return c; + } + + beforeEach(() => { + tmpDir = join(tmpdir(), `wby-mcp-server-test-${Date.now()}`); + mkdirSync(tmpDir, { recursive: true }); + }); + + afterEach(async () => { + rmSync(tmpDir, { recursive: true, force: true }); + if (cleanup) { + await cleanup(); + } + }); + + it("should list tools", async () => { + client = await startServer([tmpDir]); + const result = await client.listTools(); + + const names = result.tools.map(t => t.name); + expect(names).toContain("list_stdlib_skills"); + expect(names).toContain("get_stdlib_skill"); + }); + + it("should return grouped catalog from list_stdlib_skills", async () => { + mkdirSync(join(tmpDir, "a"), { recursive: true }); + mkdirSync(join(tmpDir, "b"), { recursive: true }); + writeFileSync( + join(tmpDir, "a", "README.md"), + "---\nname: alpha\ndescription: Alpha tool.\ncontext: node\n---\n\nAlpha body." + ); + writeFileSync( + join(tmpDir, "b", "README.md"), + "---\nname: beta\ndescription: Beta tool.\ncontext: common\n---\n\nBeta body." + ); + + client = await startServer([tmpDir]); + const result = await client.callTool({ name: "list_stdlib_skills", arguments: {} }); + + const text = (result.content as Array<{ type: string; text: string }>)[0]!.text; + expect(text).toContain("alpha"); + expect(text).toContain("beta"); + expect(text).toContain("Node.js-specific tools"); + expect(text).toContain("Platform-agnostic utilities"); + }); + + it("should return 'No skills found.' for empty catalog", async () => { + client = await startServer([tmpDir]); + const result = await client.callTool({ name: "list_stdlib_skills", arguments: {} }); + + const text = (result.content as Array<{ type: string; text: string }>)[0]!.text; + expect(text).toBe("No skills found."); + }); + + it("should return skill body from get_stdlib_skill", async () => { + mkdirSync(join(tmpDir, "c"), { recursive: true }); + writeFileSync( + join(tmpDir, "c", "README.md"), + "---\nname: gamma\ndescription: Gamma.\ncontext: node\n---\n\nGamma content here." + ); + + client = await startServer([tmpDir]); + const result = await client.callTool({ + name: "get_stdlib_skill", + arguments: { topic: "gamma" } + }); + + const text = (result.content as Array<{ type: string; text: string }>)[0]!.text; + expect(text).toBe("Gamma content here."); + }); + + it("should return error for unknown skill name", async () => { + mkdirSync(join(tmpDir, "d"), { recursive: true }); + writeFileSync( + join(tmpDir, "d", "README.md"), + "---\nname: delta\ndescription: Delta.\n---\n\nDelta body." + ); + + client = await startServer([tmpDir]); + const result = await client.callTool({ + name: "get_stdlib_skill", + arguments: { topic: "nonexistent" } + }); + + expect(result.isError).toBe(true); + const text = (result.content as Array<{ type: string; text: string }>)[0]!.text; + expect(text).toContain("not found"); + expect(text).toContain("delta"); + }); + + it("should sort skills alphabetically within groups", async () => { + mkdirSync(join(tmpDir, "z"), { recursive: true }); + mkdirSync(join(tmpDir, "a"), { recursive: true }); + writeFileSync( + join(tmpDir, "z", "README.md"), + "---\nname: zebra\ndescription: Zebra.\ncontext: node\n---\n\nZ." + ); + writeFileSync( + join(tmpDir, "a", "README.md"), + "---\nname: ant\ndescription: Ant.\ncontext: node\n---\n\nA." + ); + + client = await startServer([tmpDir]); + const result = await client.callTool({ name: "list_stdlib_skills", arguments: {} }); + + const text = (result.content as Array<{ type: string; text: string }>)[0]!.text; + const antIdx = text.indexOf("ant"); + const zebraIdx = text.indexOf("zebra"); + expect(antIdx).toBeLessThan(zebraIdx); + }); +}); diff --git a/__tests__/mcp/SkillDiscovery.test.ts b/__tests__/mcp/SkillDiscovery.test.ts new file mode 100644 index 0000000..bd3969f --- /dev/null +++ b/__tests__/mcp/SkillDiscovery.test.ts @@ -0,0 +1,182 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { Container } from "@webiny/di"; +import { SkillDiscovery, SkillDiscoveryConfig } from "../../src/mcp/features/Server/index.js"; +import { SkillDiscovery as SkillDiscoveryImpl } from "../../src/mcp/features/Server/SkillDiscovery.js"; +import type { Skill } from "../../src/mcp/features/Server/abstractions/SkillDiscovery.js"; + +function makeContainer(scanPaths: string[]): Container { + const container = new Container(); + container.registerInstance(SkillDiscoveryConfig, { scanPaths }); + container.register(SkillDiscoveryImpl).inSingletonScope(); + return container; +} + +describe("SkillDiscovery", () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = join(tmpdir(), `wby-mcp-test-${Date.now()}`); + mkdirSync(tmpDir, { recursive: true }); + }); + + afterEach(() => { + rmSync(tmpDir, { recursive: true, force: true }); + }); + + it("should parse valid front-matter from README.md", () => { + const dir = join(tmpDir, "features", "Foo"); + mkdirSync(dir, { recursive: true }); + writeFileSync( + join(dir, "README.md"), + "---\nname: foo-tool\ndescription: Does foo things.\ncontext: node\n---\n\n# FooTool\n\nContent here." + ); + + const discovery = makeContainer([tmpDir]).resolve(SkillDiscovery); + const skills: Skill[] = discovery.discover(); + + expect(skills).toHaveLength(1); + expect(skills[0]).toEqual({ + name: "foo-tool", + description: "Does foo things.", + context: "node", + body: "# FooTool\n\nContent here." + }); + }); + + it("should default context to common when omitted", () => { + mkdirSync(join(tmpDir, "a"), { recursive: true }); + writeFileSync( + join(tmpDir, "a", "README.md"), + "---\nname: bar\ndescription: Bar desc.\n---\n\nBody." + ); + + const skills = makeContainer([tmpDir]).resolve(SkillDiscovery).discover(); + + expect(skills[0]!.context).toBe("common"); + }); + + it("should skip files without front-matter", () => { + mkdirSync(join(tmpDir, "b"), { recursive: true }); + writeFileSync(join(tmpDir, "b", "README.md"), "# No front-matter\n\nJust content."); + + const skills = makeContainer([tmpDir]).resolve(SkillDiscovery).discover(); + + expect(skills).toHaveLength(0); + }); + + it("should skip files with missing name", () => { + const spy = vi.spyOn(console, "warn").mockImplementation(() => {}); + mkdirSync(join(tmpDir, "c"), { recursive: true }); + writeFileSync( + join(tmpDir, "c", "README.md"), + "---\ndescription: Missing name.\n---\n\nBody." + ); + + const skills = makeContainer([tmpDir]).resolve(SkillDiscovery).discover(); + + expect(skills).toHaveLength(0); + expect(spy).toHaveBeenCalled(); + spy.mockRestore(); + }); + + it("should skip files with missing description", () => { + const spy = vi.spyOn(console, "warn").mockImplementation(() => {}); + mkdirSync(join(tmpDir, "d"), { recursive: true }); + writeFileSync(join(tmpDir, "d", "README.md"), "---\nname: no-desc\n---\n\nBody."); + + const skills = makeContainer([tmpDir]).resolve(SkillDiscovery).discover(); + + expect(skills).toHaveLength(0); + expect(spy).toHaveBeenCalled(); + spy.mockRestore(); + }); + + it("should skip files with empty name", () => { + const spy = vi.spyOn(console, "warn").mockImplementation(() => {}); + mkdirSync(join(tmpDir, "e"), { recursive: true }); + writeFileSync( + join(tmpDir, "e", "README.md"), + '---\nname: ""\ndescription: Empty name.\n---\n\nBody.' + ); + + const skills = makeContainer([tmpDir]).resolve(SkillDiscovery).discover(); + + expect(skills).toHaveLength(0); + spy.mockRestore(); + }); + + it("should skip files with non-string name", () => { + const spy = vi.spyOn(console, "warn").mockImplementation(() => {}); + mkdirSync(join(tmpDir, "f"), { recursive: true }); + writeFileSync( + join(tmpDir, "f", "README.md"), + "---\nname: 42\ndescription: Numeric name.\n---\n\nBody." + ); + + const skills = makeContainer([tmpDir]).resolve(SkillDiscovery).discover(); + + expect(skills).toHaveLength(0); + spy.mockRestore(); + }); + + it("should discover SKILL.md files", () => { + mkdirSync(join(tmpDir, "guides"), { recursive: true }); + writeFileSync( + join(tmpDir, "guides", "SKILL.md"), + "---\nname: di-patterns\ndescription: DI guide.\ncontext: guides\n---\n\n# DI" + ); + + const skills = makeContainer([tmpDir]).resolve(SkillDiscovery).discover(); + + expect(skills).toHaveLength(1); + expect(skills[0]!.name).toBe("di-patterns"); + }); + + it("should handle name collision with first-match-wins", () => { + const dir1 = join(tmpDir, "first"); + const dir2 = join(tmpDir, "second"); + mkdirSync(dir1, { recursive: true }); + mkdirSync(dir2, { recursive: true }); + writeFileSync( + join(dir1, "SKILL.md"), + "---\nname: dupe\ndescription: First.\n---\n\nFirst body." + ); + writeFileSync( + join(dir2, "SKILL.md"), + "---\nname: dupe\ndescription: Second.\n---\n\nSecond body." + ); + + const skills = makeContainer([dir1, dir2]).resolve(SkillDiscovery).discover(); + + expect(skills).toHaveLength(1); + expect(skills[0]!.description).toBe("First."); + }); + + it("should return empty array for empty directory", () => { + const skills = makeContainer([tmpDir]).resolve(SkillDiscovery).discover(); + + expect(skills).toHaveLength(0); + }); + + it("should skip missing directories silently", () => { + const missing = join(tmpDir, "nonexistent"); + const skills = makeContainer([missing]).resolve(SkillDiscovery).discover(); + + expect(skills).toHaveLength(0); + }); + + it("should trim body whitespace", () => { + mkdirSync(join(tmpDir, "g"), { recursive: true }); + writeFileSync( + join(tmpDir, "g", "README.md"), + "---\nname: trimmed\ndescription: Trim test.\n---\n\n Body with spaces. \n\n" + ); + + const skills = makeContainer([tmpDir]).resolve(SkillDiscovery).discover(); + + expect(skills[0]!.body).toBe("Body with spaces."); + }); +}); diff --git a/config/tsconfig.check.mcp.json b/config/tsconfig.check.mcp.json new file mode 100644 index 0000000..b420f23 --- /dev/null +++ b/config/tsconfig.check.mcp.json @@ -0,0 +1,4 @@ +{ + "extends": ["./tsconfig.mcp.json", "./tsconfig.checkmode.json"], + "include": ["../src/mcp", "../__tests__/mcp"] +} diff --git a/config/tsconfig.common.json b/config/tsconfig.common.json index 9a69297..de3ce9b 100644 --- a/config/tsconfig.common.json +++ b/config/tsconfig.common.json @@ -10,5 +10,5 @@ "paths": { "~/*": ["../src/*"] } }, "include": ["../src"], - "exclude": ["../src/node", "../src/browser"] + "exclude": ["../src/node", "../src/browser", "../src/mcp"] } diff --git a/config/tsconfig.mcp.json b/config/tsconfig.mcp.json new file mode 100644 index 0000000..541971b --- /dev/null +++ b/config/tsconfig.mcp.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "composite": true, + "rootDir": "../src/mcp", + "outDir": "../dist/mcp", + "module": "nodenext", + "moduleResolution": "nodenext", + "types": ["node"], + "paths": { "~/*": ["../src/*"] } + }, + "include": ["../src/mcp"], + "references": [{ "path": "./tsconfig.common.json" }] +} diff --git a/docs/superpowers/plans/2026-07-28-mcp-server.md b/docs/superpowers/plans/2026-07-28-mcp-server.md new file mode 100644 index 0000000..b08977b --- /dev/null +++ b/docs/superpowers/plans/2026-07-28-mcp-server.md @@ -0,0 +1,2078 @@ +# MCP Server Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add an MCP server to `@webiny/stdlib` that exposes `list_stdlib_skills` and `get_stdlib_skill` tools for AI agent skill discovery. + +**Architecture:** New `src/mcp/` slice with DI-based features (SkillDiscovery, McpServer, AgentConfigurator). Plain modules for 7 agent adapters. CLI entry point dispatches `serve` and `configure` commands. Skills sourced from feature READMEs (with front-matter) and standalone SKILL.md files. + +**Tech Stack:** `@modelcontextprotocol/sdk` ^1.30.0, `@11ty/gray-matter` ^2.1.0, `zod` (existing dep), `@webiny/di` (existing dep) + +## Global Constraints + +- All source imports use `.js` extensions (tsgo resolves to `.ts`) +- Cross-slice imports via `~/common/index.js` path alias +- DI tokens use `"Mcp/"` domain prefix +- All DI registrations use `.inSingletonScope()` +- Namespace pattern: `namespace ToolName { export type Interface = IToolName }` +- Named exports only (no default exports) +- Tests use `PinoLoggerConfig` with `logLevel: "error"` to silence logs +- Feature folders require `README.md` +- Pre-commit chain: `yarn && yarn adio && yarn format:fix && yarn lint:fix && yarn typecheck && yarn build && yarn test:coverage` + +**Spec:** `docs/superpowers/specs/2026-07-28-mcp-server-design.md` + +--- + +### Task 1: Build Integration + +**Files:** +- Modify: `config/tsconfig.common.json` (add `../src/mcp` to exclude) +- Create: `config/tsconfig.mcp.json` +- Create: `config/tsconfig.check.mcp.json` +- Modify: `tsconfig.json` (add mcp reference) +- Modify: `package.json` (exports, bin, deps, files, typecheck script) +- Modify: `scripts/features/BuildPackages/index.ts` (add mcp slice) +- Modify: `scripts/features/BuildPackages/BuildOrchestrator.ts` (add copyReadmes + ensureShebang) +- Modify: `scripts/features/BuildPackages/abstractions/BuildOrchestrator.ts` (if interface needs update) +- Create: `src/mcp/index.ts` (empty barrel — placeholder so tsgo has something to compile) + +**Interfaces:** +- Consumes: existing build infrastructure +- Produces: compilable mcp slice, `dist/mcp/` output directory, `dist/**/README.md` copies + +- [ ] **Step 1: Install dependencies** + +```bash +yarn add @modelcontextprotocol/sdk@^1.30.0 @11ty/gray-matter@^2.1.0 +``` + +- [ ] **Step 2: Add mcp exclude to tsconfig.common.json** + +In `config/tsconfig.common.json`, add `"../src/mcp"` to the exclude array: + +```json +"exclude": ["../src/node", "../src/browser", "../src/mcp"] +``` + +- [ ] **Step 3: Create config/tsconfig.mcp.json** + +```json +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "composite": true, + "rootDir": "../src/mcp", + "outDir": "../dist/mcp", + "module": "nodenext", + "moduleResolution": "nodenext", + "types": ["node"], + "paths": { "~/*": ["../src/*"] } + }, + "include": ["../src/mcp"], + "references": [{ "path": "./tsconfig.common.json" }] +} +``` + +- [ ] **Step 4: Create config/tsconfig.check.mcp.json** + +```json +{ + "extends": ["./tsconfig.mcp.json", "./tsconfig.checkmode.json"], + "include": ["../src/mcp", "../__tests__/mcp"] +} +``` + +- [ ] **Step 5: Add mcp reference to root tsconfig.json** + +Add after the browser reference: + +```json +{ "path": "./config/tsconfig.mcp.json" } +``` + +- [ ] **Step 6: Update package.json** + +Add to `exports`: +```json +"./mcp": { + "import": "./dist/mcp/index.js", + "types": "./dist/mcp/index.d.ts" +} +``` + +Add `bin` field: +```json +"bin": { + "stdlib-mcp": "./dist/mcp/cli.js" +} +``` + +Add `"skills"` to `files` array: `["dist", "skills"]` + +Append to `typecheck` script: `&& tsgo -p config/tsconfig.check.mcp.json` + +- [ ] **Step 7: Create placeholder src/mcp/index.ts** + +```ts +// MCP server barrel — populated as features are implemented. +``` + +- [ ] **Step 8: Add mcp slice to build script** + +In `scripts/features/BuildPackages/index.ts`, append to slices array: + +```ts +slices: [ + "config/tsconfig.common.json", + "config/tsconfig.node.json", + "config/tsconfig.browser.json", + "config/tsconfig.mcp.json" +] +``` + +- [ ] **Step 9: Add copyReadmes and ensureShebang to BuildOrchestrator** + +Read `scripts/features/BuildPackages/BuildOrchestrator.ts` and `scripts/features/BuildPackages/abstractions/BuildOrchestrator.ts`. + +In the orchestrator's `run()` method, after `this.pathAliasRewriter.rewrite(distDir)` and before artifact copies, add two new private methods and call them: + +```ts +private copyReadmes(rootDir: string): void { + const srcDir = join(rootDir, "src"); + const distDir = join(rootDir, "dist"); + this.walkForReadmes(srcDir, srcDir, distDir); +} + +private walkForReadmes(baseDir: string, dir: string, distDir: string): void { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const fullPath = join(dir, entry.name); + if (entry.isDirectory()) { + this.walkForReadmes(baseDir, fullPath, distDir); + } else if (entry.name === "README.md") { + const relPath = relative(baseDir, fullPath); + const destPath = join(distDir, relPath); + mkdirSync(dirname(destPath), { recursive: true }); + copyFileSync(fullPath, destPath); + } + } +} + +private ensureShebang(rootDir: string): void { + const cliPath = join(rootDir, "dist", "mcp", "cli.js"); + if (!existsSync(cliPath)) { + return; + } + const content = readFileSync(cliPath, "utf-8"); + if (!content.startsWith("#!")) { + writeFileSync(cliPath, "#!/usr/bin/env node\n" + content); + } +} +``` + +Add needed imports at top: `readdirSync`, `relative`, `dirname`, `copyFileSync`, `existsSync`, `readFileSync`, `writeFileSync` from `node:fs` and `node:path`. + +Call in `run()`: + +```ts +this.copyReadmes(rootDir); +this.ensureShebang(rootDir); +``` + +- [ ] **Step 10: Build and verify** + +```bash +yarn build +``` + +Verify: +```bash +ls dist/mcp/index.js +head -1 dist/mcp/cli.js 2>/dev/null || echo "no cli yet (expected)" +find dist -name "README.md" | head -5 +``` + +- [ ] **Step 11: Run typecheck** + +```bash +yarn typecheck +``` + +- [ ] **Step 12: Commit** + +```bash +git add -A +git commit -m "feat(mcp): add build integration for mcp slice + +Add tsconfig.mcp.json, package.json exports/bin/deps, build script +slice, and BuildOrchestrator steps for copying READMEs and ensuring +CLI shebang." +``` + +--- + +### Task 2: SkillDiscovery Feature + +**Files:** +- Create: `src/mcp/features/Server/abstractions/SkillDiscovery.ts` +- Create: `src/mcp/features/Server/abstractions/SkillDiscoveryConfig.ts` +- Create: `src/mcp/features/Server/abstractions/index.ts` +- Create: `src/mcp/features/Server/SkillDiscovery.ts` +- Create: `__tests__/mcp/SkillDiscovery.test.ts` + +**Interfaces:** +- Consumes: `createAbstraction` from `~/common/index.js` +- Produces: `SkillDiscovery` token, `SkillDiscoveryConfig` token, `Skill` type, `SkillDiscoveryImpl` implementation + +- [ ] **Step 1: Write the failing test** + +Create `__tests__/mcp/SkillDiscovery.test.ts`: + +```ts +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { Container } from "@webiny/di"; +import { + SkillDiscovery, + SkillDiscoveryConfig +} from "../../src/mcp/features/Server/index.js"; +import type { Skill } from "../../src/mcp/features/Server/abstractions/SkillDiscovery.js"; + +function makeContainer(scanPaths: string[]): Container { + const container = new Container(); + container.registerInstance(SkillDiscoveryConfig, { scanPaths }); + container.register(SkillDiscovery).inSingletonScope(); + return container; +} + +describe("SkillDiscovery", () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = join(tmpdir(), `wby-mcp-test-${Date.now()}`); + mkdirSync(tmpDir, { recursive: true }); + }); + + afterEach(() => { + rmSync(tmpDir, { recursive: true, force: true }); + }); + + it("should parse valid front-matter from README.md", () => { + const dir = join(tmpDir, "features", "Foo"); + mkdirSync(dir, { recursive: true }); + writeFileSync( + join(dir, "README.md"), + "---\nname: foo-tool\ndescription: Does foo things.\ncontext: node\n---\n\n# FooTool\n\nContent here." + ); + + const discovery = makeContainer([tmpDir]).resolve(SkillDiscovery); + const skills = discovery.discover(); + + expect(skills).toHaveLength(1); + expect(skills[0]).toEqual({ + name: "foo-tool", + description: "Does foo things.", + context: "node", + body: "# FooTool\n\nContent here." + }); + }); + + it("should default context to common when omitted", () => { + mkdirSync(join(tmpDir, "a"), { recursive: true }); + writeFileSync( + join(tmpDir, "a", "README.md"), + "---\nname: bar\ndescription: Bar desc.\n---\n\nBody." + ); + + const skills = makeContainer([tmpDir]).resolve(SkillDiscovery).discover(); + + expect(skills[0]!.context).toBe("common"); + }); + + it("should skip files without front-matter", () => { + mkdirSync(join(tmpDir, "b"), { recursive: true }); + writeFileSync(join(tmpDir, "b", "README.md"), "# No front-matter\n\nJust content."); + + const skills = makeContainer([tmpDir]).resolve(SkillDiscovery).discover(); + + expect(skills).toHaveLength(0); + }); + + it("should skip files with missing name", () => { + const spy = vi.spyOn(console, "warn").mockImplementation(() => {}); + mkdirSync(join(tmpDir, "c"), { recursive: true }); + writeFileSync( + join(tmpDir, "c", "README.md"), + "---\ndescription: Missing name.\n---\n\nBody." + ); + + const skills = makeContainer([tmpDir]).resolve(SkillDiscovery).discover(); + + expect(skills).toHaveLength(0); + expect(spy).toHaveBeenCalled(); + spy.mockRestore(); + }); + + it("should skip files with missing description", () => { + const spy = vi.spyOn(console, "warn").mockImplementation(() => {}); + mkdirSync(join(tmpDir, "d"), { recursive: true }); + writeFileSync( + join(tmpDir, "d", "README.md"), + "---\nname: no-desc\n---\n\nBody." + ); + + const skills = makeContainer([tmpDir]).resolve(SkillDiscovery).discover(); + + expect(skills).toHaveLength(0); + expect(spy).toHaveBeenCalled(); + spy.mockRestore(); + }); + + it("should skip files with empty name", () => { + const spy = vi.spyOn(console, "warn").mockImplementation(() => {}); + mkdirSync(join(tmpDir, "e"), { recursive: true }); + writeFileSync( + join(tmpDir, "e", "README.md"), + '---\nname: ""\ndescription: Empty name.\n---\n\nBody.' + ); + + const skills = makeContainer([tmpDir]).resolve(SkillDiscovery).discover(); + + expect(skills).toHaveLength(0); + spy.mockRestore(); + }); + + it("should skip files with non-string name", () => { + const spy = vi.spyOn(console, "warn").mockImplementation(() => {}); + mkdirSync(join(tmpDir, "f"), { recursive: true }); + writeFileSync( + join(tmpDir, "f", "README.md"), + "---\nname: 42\ndescription: Numeric name.\n---\n\nBody." + ); + + const skills = makeContainer([tmpDir]).resolve(SkillDiscovery).discover(); + + expect(skills).toHaveLength(0); + spy.mockRestore(); + }); + + it("should discover SKILL.md files", () => { + mkdirSync(join(tmpDir, "guides"), { recursive: true }); + writeFileSync( + join(tmpDir, "guides", "SKILL.md"), + "---\nname: di-patterns\ndescription: DI guide.\ncontext: guides\n---\n\n# DI" + ); + + const skills = makeContainer([tmpDir]).resolve(SkillDiscovery).discover(); + + expect(skills).toHaveLength(1); + expect(skills[0]!.name).toBe("di-patterns"); + }); + + it("should handle name collision with first-match-wins", () => { + const dir1 = join(tmpDir, "first"); + const dir2 = join(tmpDir, "second"); + mkdirSync(dir1, { recursive: true }); + mkdirSync(dir2, { recursive: true }); + writeFileSync( + join(dir1, "SKILL.md"), + "---\nname: dupe\ndescription: First.\n---\n\nFirst body." + ); + writeFileSync( + join(dir2, "SKILL.md"), + "---\nname: dupe\ndescription: Second.\n---\n\nSecond body." + ); + + const skills = makeContainer([dir1, dir2]).resolve(SkillDiscovery).discover(); + + expect(skills).toHaveLength(1); + expect(skills[0]!.description).toBe("First."); + }); + + it("should return empty array for empty directory", () => { + const skills = makeContainer([tmpDir]).resolve(SkillDiscovery).discover(); + + expect(skills).toHaveLength(0); + }); + + it("should skip missing directories silently", () => { + const missing = join(tmpDir, "nonexistent"); + const skills = makeContainer([missing]).resolve(SkillDiscovery).discover(); + + expect(skills).toHaveLength(0); + }); + + it("should trim body whitespace", () => { + mkdirSync(join(tmpDir, "g"), { recursive: true }); + writeFileSync( + join(tmpDir, "g", "README.md"), + "---\nname: trimmed\ndescription: Trim test.\n---\n\n Body with spaces. \n\n" + ); + + const skills = makeContainer([tmpDir]).resolve(SkillDiscovery).discover(); + + expect(skills[0]!.body).toBe("Body with spaces."); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +```bash +yarn test -- __tests__/mcp/SkillDiscovery.test.ts +``` + +Expected: FAIL — modules don't exist. + +- [ ] **Step 3: Create abstractions** + +Create `src/mcp/features/Server/abstractions/SkillDiscoveryConfig.ts`: + +```ts +import { createAbstraction } from "~/common/index.js"; + +export interface ISkillDiscoveryConfig { + scanPaths: string[]; +} + +export const SkillDiscoveryConfig = createAbstraction( + "Mcp/SkillDiscoveryConfig" +); + +export namespace SkillDiscoveryConfig { + export type Interface = ISkillDiscoveryConfig; +} +``` + +Create `src/mcp/features/Server/abstractions/SkillDiscovery.ts`: + +```ts +import { createAbstraction } from "~/common/index.js"; + +export interface Skill { + name: string; + description: string; + context: string; + body: string; +} + +export interface ISkillDiscovery { + discover(): Skill[]; +} + +export const SkillDiscovery = createAbstraction( + "Mcp/SkillDiscovery" +); + +export namespace SkillDiscovery { + export type Interface = ISkillDiscovery; +} +``` + +Create `src/mcp/features/Server/abstractions/index.ts`: + +```ts +export { SkillDiscovery, type Skill } from "./SkillDiscovery.js"; +export { SkillDiscoveryConfig } from "./SkillDiscoveryConfig.js"; +``` + +- [ ] **Step 4: Create implementation** + +Create `src/mcp/features/Server/SkillDiscovery.ts`: + +```ts +import { readdirSync, readFileSync, existsSync } from "node:fs"; +import { join } from "node:path"; +import matter from "@11ty/gray-matter"; +import { + SkillDiscovery as SkillDiscoveryAbstraction, + type Skill +} from "./abstractions/SkillDiscovery.js"; +import { SkillDiscoveryConfig } from "./abstractions/SkillDiscoveryConfig.js"; + +class SkillDiscoveryImpl implements SkillDiscoveryAbstraction.Interface { + public constructor( + private readonly config: SkillDiscoveryConfig.Interface + ) {} + + public discover(): Skill[] { + const seen = new Set(); + const skills: Skill[] = []; + + for (const scanPath of this.config.scanPaths) { + if (!existsSync(scanPath)) { + continue; + } + this.walk(scanPath, seen, skills); + } + + return skills; + } + + private walk(dir: string, seen: Set, skills: Skill[]): void { + let entries; + try { + entries = readdirSync(dir, { withFileTypes: true }); + } catch { + return; + } + + for (const entry of entries) { + const fullPath = join(dir, entry.name); + if (entry.isDirectory()) { + this.walk(fullPath, seen, skills); + } else if (entry.name === "README.md" || entry.name === "SKILL.md") { + this.tryParseSkill(fullPath, seen, skills); + } + } + } + + private tryParseSkill( + filePath: string, + seen: Set, + skills: Skill[] + ): void { + let raw: string; + try { + raw = readFileSync(filePath, "utf-8"); + } catch { + console.warn(`Skipping unreadable file: ${filePath}`); + return; + } + + if (!matter.test(raw)) { + return; + } + + let parsed; + try { + parsed = matter(raw); + } catch { + console.warn(`Skipping ${filePath}: invalid YAML front-matter`); + return; + } + + const { name, description, context } = parsed.data as Record; + + if (typeof name !== "string" || name === "") { + console.warn(`Skipping ${filePath}: missing or empty "name" field`); + return; + } + + if (typeof description !== "string" || description === "") { + console.warn(`Skipping ${filePath}: missing or empty "description" field`); + return; + } + + if (context !== undefined && typeof context !== "string") { + console.warn(`Skipping ${filePath}: "context" must be a string`); + return; + } + + if (seen.has(name)) { + return; + } + + seen.add(name); + skills.push({ + name, + description, + context: typeof context === "string" ? context : "common", + body: parsed.content.trim() + }); + } +} + +export const SkillDiscovery = SkillDiscoveryAbstraction.createImplementation({ + implementation: SkillDiscoveryImpl, + dependencies: [SkillDiscoveryConfig] +}); +``` + +- [ ] **Step 5: Create feature index (no feature.ts yet — McpServer feature will wire both)** + +Create `src/mcp/features/Server/index.ts`: + +```ts +export { SkillDiscovery, SkillDiscoveryConfig, type Skill } from "./abstractions/index.js"; +export { SkillDiscovery as SkillDiscoveryImpl } from "./SkillDiscovery.js"; +``` + +- [ ] **Step 6: Update barrel** + +Update `src/mcp/index.ts`: + +```ts +export { + SkillDiscovery, + SkillDiscoveryConfig, + type Skill +} from "./features/Server/index.js"; +``` + +- [ ] **Step 7: Run test to verify it passes** + +```bash +yarn test -- __tests__/mcp/SkillDiscovery.test.ts +``` + +Expected: all 11 tests PASS. + +- [ ] **Step 8: Run typecheck** + +```bash +yarn typecheck +``` + +- [ ] **Step 9: Commit** + +```bash +git add -A +git commit -m "feat(mcp): add SkillDiscovery feature + +Scans directories for README.md and SKILL.md files with YAML +YAML front-matter via @11ty/gray-matter. Extracts name, description, +context, and body. First-match-wins on name collision. Skips missing +directories and invalid front-matter with warnings." +``` + +--- + +### Task 3: McpServer Feature + +**Files:** +- Create: `src/mcp/features/Server/abstractions/McpServer.ts` (add to abstractions/index.ts) +- Create: `src/mcp/features/Server/McpServer.ts` +- Create: `src/mcp/features/Server/feature.ts` +- Create: `src/mcp/features/Server/README.md` +- Modify: `src/mcp/features/Server/index.ts` (add McpServer exports) +- Modify: `src/mcp/index.ts` (add McpServer exports) +- Create: `__tests__/mcp/McpServer.test.ts` + +**Interfaces:** +- Consumes: `SkillDiscovery` token, `Skill` type from Task 2 +- Produces: `McpServer` token, `McpServerFeature`, `list_stdlib_skills` tool, `get_stdlib_skill` tool + +- [ ] **Step 1: Write the failing test** + +Create `__tests__/mcp/McpServer.test.ts`: + +```ts +import { beforeEach, describe, expect, it } from "vitest"; +import { Container } from "@webiny/di"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { + SkillDiscoveryConfig, + McpServer, + McpServerFeature +} from "../../src/mcp/index.js"; +import { mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { afterEach } from "vitest"; + +describe("McpServer", () => { + let tmpDir: string; + let client: Client; + let cleanup: () => Promise; + + async function startServer(scanPaths: string[]): Promise { + const container = new Container(); + container.registerInstance(SkillDiscoveryConfig, { scanPaths }); + McpServerFeature.register(container); + const server = container.resolve(McpServer); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + const c = new Client({ name: "test-client", version: "1.0.0" }); + await server.startWithTransport(serverTransport); + await c.connect(clientTransport); + + cleanup = async () => { + await c.close(); + }; + + return c; + } + + beforeEach(() => { + tmpDir = join(tmpdir(), `wby-mcp-server-test-${Date.now()}`); + mkdirSync(tmpDir, { recursive: true }); + }); + + afterEach(async () => { + rmSync(tmpDir, { recursive: true, force: true }); + if (cleanup) { + await cleanup(); + } + }); + + it("should list tools", async () => { + client = await startServer([tmpDir]); + const result = await client.listTools(); + + const names = result.tools.map(t => t.name); + expect(names).toContain("list_stdlib_skills"); + expect(names).toContain("get_stdlib_skill"); + }); + + it("should return grouped catalog from list_stdlib_skills", async () => { + mkdirSync(join(tmpDir, "a"), { recursive: true }); + mkdirSync(join(tmpDir, "b"), { recursive: true }); + writeFileSync( + join(tmpDir, "a", "README.md"), + "---\nname: alpha\ndescription: Alpha tool.\ncontext: node\n---\n\nAlpha body." + ); + writeFileSync( + join(tmpDir, "b", "README.md"), + "---\nname: beta\ndescription: Beta tool.\ncontext: common\n---\n\nBeta body." + ); + + client = await startServer([tmpDir]); + const result = await client.callTool({ name: "list_stdlib_skills", arguments: {} }); + + const text = (result.content as Array<{ type: string; text: string }>)[0]!.text; + expect(text).toContain("alpha"); + expect(text).toContain("beta"); + expect(text).toContain("Node.js-specific tools"); + expect(text).toContain("Platform-agnostic utilities"); + }); + + it("should return 'No skills found.' for empty catalog", async () => { + client = await startServer([tmpDir]); + const result = await client.callTool({ name: "list_stdlib_skills", arguments: {} }); + + const text = (result.content as Array<{ type: string; text: string }>)[0]!.text; + expect(text).toBe("No skills found."); + }); + + it("should return skill body from get_stdlib_skill", async () => { + mkdirSync(join(tmpDir, "c"), { recursive: true }); + writeFileSync( + join(tmpDir, "c", "README.md"), + "---\nname: gamma\ndescription: Gamma.\ncontext: node\n---\n\nGamma content here." + ); + + client = await startServer([tmpDir]); + const result = await client.callTool({ + name: "get_stdlib_skill", + arguments: { topic: "gamma" } + }); + + const text = (result.content as Array<{ type: string; text: string }>)[0]!.text; + expect(text).toBe("Gamma content here."); + }); + + it("should return error for unknown skill name", async () => { + mkdirSync(join(tmpDir, "d"), { recursive: true }); + writeFileSync( + join(tmpDir, "d", "README.md"), + "---\nname: delta\ndescription: Delta.\n---\n\nDelta body." + ); + + client = await startServer([tmpDir]); + const result = await client.callTool({ + name: "get_stdlib_skill", + arguments: { topic: "nonexistent" } + }); + + expect(result.isError).toBe(true); + const text = (result.content as Array<{ type: string; text: string }>)[0]!.text; + expect(text).toContain("not found"); + expect(text).toContain("delta"); + }); + + it("should sort skills alphabetically within groups", async () => { + mkdirSync(join(tmpDir, "z"), { recursive: true }); + mkdirSync(join(tmpDir, "a"), { recursive: true }); + writeFileSync( + join(tmpDir, "z", "README.md"), + "---\nname: zebra\ndescription: Zebra.\ncontext: node\n---\n\nZ." + ); + writeFileSync( + join(tmpDir, "a", "README.md"), + "---\nname: ant\ndescription: Ant.\ncontext: node\n---\n\nA." + ); + + client = await startServer([tmpDir]); + const result = await client.callTool({ name: "list_stdlib_skills", arguments: {} }); + + const text = (result.content as Array<{ type: string; text: string }>)[0]!.text; + const antIdx = text.indexOf("ant"); + const zebraIdx = text.indexOf("zebra"); + expect(antIdx).toBeLessThan(zebraIdx); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +```bash +yarn test -- __tests__/mcp/McpServer.test.ts +``` + +Expected: FAIL — modules don't exist. + +- [ ] **Step 3: Create McpServer abstraction** + +Create `src/mcp/features/Server/abstractions/McpServer.ts`: + +```ts +import { createAbstraction } from "~/common/index.js"; +import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; + +export interface IMcpServer { + start(): Promise; + startWithTransport(transport: Transport): Promise; +} + +export const McpServer = createAbstraction("Mcp/McpServer"); + +export namespace McpServer { + export type Interface = IMcpServer; +} +``` + +Update `src/mcp/features/Server/abstractions/index.ts`: + +```ts +export { SkillDiscovery, type Skill } from "./SkillDiscovery.js"; +export { SkillDiscoveryConfig } from "./SkillDiscoveryConfig.js"; +export { McpServer } from "./McpServer.js"; +``` + +- [ ] **Step 4: Create McpServer implementation** + +Create `src/mcp/features/Server/McpServer.ts`: + +```ts +import { McpServer as SdkMcpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; +import { z } from "zod"; +import { McpServer as McpServerAbstraction } from "./abstractions/McpServer.js"; +import { + SkillDiscovery, + type Skill +} from "./abstractions/SkillDiscovery.js"; + +const CONTEXT_HEADINGS: Record = { + common: "Platform-agnostic utilities", + node: "Node.js-specific tools", + browser: "Browser-specific tools", + guides: "Cross-cutting guides and patterns" +}; + +const CONTEXT_ORDER = ["common", "node", "browser", "guides"]; + +function buildCatalog(skills: Skill[]): string { + if (skills.length === 0) { + return "No skills found."; + } + + const groups = new Map(); + for (const skill of skills) { + const existing = groups.get(skill.context); + if (existing) { + existing.push(skill); + } else { + groups.set(skill.context, [skill]); + } + } + + for (const [, group] of groups) { + group.sort((a, b) => a.name.localeCompare(b.name)); + } + + const sections: string[] = []; + const orderedContexts = [ + ...CONTEXT_ORDER.filter(c => groups.has(c)), + ...[...groups.keys()].filter(c => !CONTEXT_ORDER.includes(c)).sort() + ]; + + for (const ctx of orderedContexts) { + const heading = CONTEXT_HEADINGS[ctx] ?? ctx; + const group = groups.get(ctx)!; + const rows = group.map(s => `| ${s.name} | ${s.description} |`).join("\n"); + sections.push(`## ${heading}\n\n| Skill | Description |\n|-------|-------------|\n${rows}`); + } + + return sections.join("\n\n"); +} + +class McpServerImpl implements McpServerAbstraction.Interface { + public constructor( + private readonly skillDiscovery: SkillDiscovery.Interface + ) {} + + public async start(): Promise { + const transport = new StdioServerTransport(); + await this.startWithTransport(transport); + } + + public async startWithTransport(transport: Transport): Promise { + const skills = this.skillDiscovery.discover(); + const skillMap = new Map(); + for (const skill of skills) { + skillMap.set(skill.name, skill); + } + + const server = new SdkMcpServer( + { name: "stdlib", version: "1.0.0" } + ); + + server.registerTool("list_stdlib_skills", { + title: "List stdlib skills", + description: + "Returns a catalog of all available @webiny/stdlib skills. Call this first when working with @webiny/stdlib, then call get_stdlib_skill to load a specific skill.", + annotations: { readOnlyHint: true } + }, () => ({ + content: [{ type: "text" as const, text: buildCatalog(skills) }] + })); + + server.registerTool("get_stdlib_skill", { + title: "Get stdlib skill", + description: + "Loads full documentation for a specific @webiny/stdlib skill. Call list_stdlib_skills first to see available names.", + inputSchema: { topic: z.string().describe("Skill name from list_stdlib_skills") }, + annotations: { readOnlyHint: true } + }, ({ topic }) => { + const skill = skillMap.get(topic); + if (!skill) { + const available = [...skillMap.keys()] + .sort() + .map(n => { + const s = skillMap.get(n)!; + return `- ${n} (${s.context})`; + }) + .join("\n"); + return { + isError: true, + content: [{ + type: "text" as const, + text: `Skill "${topic}" not found. Available skills:\n${available}` + }] + }; + } + return { + content: [{ type: "text" as const, text: skill.body }] + }; + }); + + await server.connect(transport); + } +} + +export const McpServer = McpServerAbstraction.createImplementation({ + implementation: McpServerImpl, + dependencies: [SkillDiscovery] +}); +``` + +- [ ] **Step 5: Create feature.ts** + +Create `src/mcp/features/Server/feature.ts`: + +```ts +import { createFeature } from "~/common/index.js"; +import { SkillDiscovery } from "./SkillDiscovery.js"; +import { McpServer } from "./McpServer.js"; + +export const McpServerFeature = createFeature({ + name: "Mcp/McpServerFeature", + register(container) { + container.register(SkillDiscovery).inSingletonScope(); + container.register(McpServer).inSingletonScope(); + } +}); +``` + +- [ ] **Step 6: Update Server index.ts** + +```ts +export { + SkillDiscovery, + SkillDiscoveryConfig, + McpServer, + type Skill +} from "./abstractions/index.js"; +export { SkillDiscovery as SkillDiscoveryImpl } from "./SkillDiscovery.js"; +export { McpServer as McpServerImpl } from "./McpServer.js"; +export { McpServerFeature } from "./feature.js"; +``` + +- [ ] **Step 7: Update mcp barrel** + +```ts +export { + SkillDiscovery, + SkillDiscoveryConfig, + McpServer, + McpServerFeature, + type Skill +} from "./features/Server/index.js"; +``` + +- [ ] **Step 8: Create README.md** + +Create `src/mcp/features/Server/README.md`: + +```markdown +# MCP Server + +Exposes `@webiny/stdlib` feature documentation as MCP tools for AI agent discovery. Two tools: `list_stdlib_skills` returns a grouped catalog; `get_stdlib_skill` loads full documentation for a specific skill. + +## Interface + +### SkillDiscovery + +- `discover(): Skill[]` — scans configured directories for README.md and SKILL.md files with YAML front-matter (via `@11ty/gray-matter`). Returns parsed skills. + +### McpServer + +- `start(): Promise` — starts the MCP server on stdio transport. Blocks until stdin closes. +- `startWithTransport(transport): Promise` — starts with a custom transport (used for testing). + +## Usage + +```ts +import { Container } from "@webiny/di"; +import { SkillDiscoveryConfig, McpServerFeature, McpServer } from "@webiny/stdlib/mcp"; + +const container = new Container(); +container.registerInstance(SkillDiscoveryConfig, { + scanPaths: ["./skills", "./src"] +}); +McpServerFeature.register(container); + +await container.resolve(McpServer).start(); +``` +``` + +- [ ] **Step 9: Run tests** + +```bash +yarn test -- __tests__/mcp/McpServer.test.ts +``` + +Expected: all 7 tests PASS. + +- [ ] **Step 10: Run typecheck and full test suite** + +```bash +yarn typecheck && yarn test +``` + +- [ ] **Step 11: Commit** + +```bash +git add -A +git commit -m "feat(mcp): add McpServer feature with list and get tools + +Registers list_stdlib_skills and get_stdlib_skill MCP tools. +Skills discovered via SkillDiscovery and cached at startup. +Catalog grouped by context, sorted alphabetically." +``` + +--- + +### Task 4: Configure Feature with Agent Adapters + +**Files:** +- Create: `src/mcp/features/Configure/abstractions/AgentConfigurator.ts` +- Create: `src/mcp/features/Configure/abstractions/index.ts` +- Create: `src/mcp/features/Configure/AgentConfigurator.ts` +- Create: `src/mcp/features/Configure/feature.ts` +- Create: `src/mcp/features/Configure/index.ts` +- Create: `src/mcp/features/Configure/README.md` +- Create: `src/mcp/features/Configure/agents/types.ts` +- Create: `src/mcp/features/Configure/agents/shared.ts` +- Create: `src/mcp/features/Configure/agents/claude.ts` +- Create: `src/mcp/features/Configure/agents/cursor.ts` +- Create: `src/mcp/features/Configure/agents/cline.ts` +- Create: `src/mcp/features/Configure/agents/copilot.ts` +- Create: `src/mcp/features/Configure/agents/windsurf.ts` +- Create: `src/mcp/features/Configure/agents/kiro.ts` +- Create: `src/mcp/features/Configure/agents/opencode.ts` +- Create: `__tests__/mcp/Configure.test.ts` +- Modify: `src/mcp/index.ts` + +**Interfaces:** +- Consumes: nothing from previous tasks (self-contained) +- Produces: `AgentConfigurator` token, `AgentConfiguratorFeature`, `writeMcpConfig`, `writeHintFile`, `stdlibHintBlock`, all 7 agent modules + +- [ ] **Step 1: Write the failing test** + +Create `__tests__/mcp/Configure.test.ts`: + +```ts +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { + writeMcpConfig, + writeHintFile, + stdlibHintBlock +} from "../../src/mcp/features/Configure/agents/shared.js"; + +describe("writeMcpConfig", () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = join(tmpdir(), `wby-mcp-config-test-${Date.now()}`); + mkdirSync(tmpDir, { recursive: true }); + }); + + afterEach(() => { + rmSync(tmpDir, { recursive: true, force: true }); + }); + + it("should create new config file with stdlib entry", () => { + writeMcpConfig({ cwd: tmpDir, configFile: ".mcp.json", configKey: "mcpServers" }); + + const content = JSON.parse(readFileSync(join(tmpDir, ".mcp.json"), "utf-8")); + expect(content.mcpServers.stdlib).toBeDefined(); + expect(content.mcpServers.stdlib.command).toBe("npx"); + expect(content.mcpServers.stdlib.args).toContain("stdlib-mcp"); + }); + + it("should preserve existing entries when patching", () => { + const configPath = join(tmpDir, ".mcp.json"); + writeFileSync(configPath, JSON.stringify({ + mcpServers: { other: { command: "other-cmd" } } + })); + + writeMcpConfig({ cwd: tmpDir, configFile: ".mcp.json", configKey: "mcpServers" }); + + const content = JSON.parse(readFileSync(configPath, "utf-8")); + expect(content.mcpServers.other).toBeDefined(); + expect(content.mcpServers.stdlib).toBeDefined(); + }); + + it("should skip if stdlib entry already exists", () => { + const configPath = join(tmpDir, ".mcp.json"); + const original = { mcpServers: { stdlib: { command: "custom" } } }; + writeFileSync(configPath, JSON.stringify(original)); + + writeMcpConfig({ cwd: tmpDir, configFile: ".mcp.json", configKey: "mcpServers" }); + + const content = JSON.parse(readFileSync(configPath, "utf-8")); + expect(content.mcpServers.stdlib.command).toBe("custom"); + }); + + it("should create nested directories", () => { + writeMcpConfig({ + cwd: tmpDir, + configFile: ".cursor/mcp.json", + configKey: "mcpServers" + }); + + expect(existsSync(join(tmpDir, ".cursor", "mcp.json"))).toBe(true); + }); + + it("should use servers key for copilot", () => { + writeMcpConfig({ cwd: tmpDir, configFile: ".vscode/mcp.json", configKey: "servers" }); + + const content = JSON.parse( + readFileSync(join(tmpDir, ".vscode", "mcp.json"), "utf-8") + ); + expect(content.servers.stdlib).toBeDefined(); + }); +}); + +describe("writeHintFile", () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = join(tmpdir(), `wby-mcp-hint-test-${Date.now()}`); + mkdirSync(tmpDir, { recursive: true }); + }); + + afterEach(() => { + rmSync(tmpDir, { recursive: true, force: true }); + }); + + it("should create new hint file", () => { + writeHintFile({ + cwd: tmpDir, + hintFile: "CLAUDE.md", + content: stdlibHintBlock() + }); + + const text = readFileSync(join(tmpDir, "CLAUDE.md"), "utf-8"); + expect(text).toContain("list_stdlib_skills"); + }); + + it("should append to existing file with blank line separator", () => { + const hintPath = join(tmpDir, "CLAUDE.md"); + writeFileSync(hintPath, "# Existing content"); + + writeHintFile({ + cwd: tmpDir, + hintFile: "CLAUDE.md", + content: stdlibHintBlock() + }); + + const text = readFileSync(hintPath, "utf-8"); + expect(text).toContain("# Existing content"); + expect(text).toContain("list_stdlib_skills"); + expect(text).toContain("\n\n## @webiny/stdlib MCP"); + }); + + it("should skip if marker already present", () => { + const hintPath = join(tmpDir, "CLAUDE.md"); + writeFileSync(hintPath, "Already has list_stdlib_skills marker."); + + writeHintFile({ + cwd: tmpDir, + hintFile: "CLAUDE.md", + content: stdlibHintBlock() + }); + + const text = readFileSync(hintPath, "utf-8"); + expect(text).toBe("Already has list_stdlib_skills marker."); + }); + + it("should create nested directories", () => { + writeHintFile({ + cwd: tmpDir, + hintFile: ".cursor/rules/stdlib.mdc", + content: stdlibHintBlock() + }); + + expect(existsSync(join(tmpDir, ".cursor", "rules", "stdlib.mdc"))).toBe(true); + }); +}); + +describe("agent adapters", () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = join(tmpdir(), `wby-mcp-agent-test-${Date.now()}`); + mkdirSync(tmpDir, { recursive: true }); + }); + + afterEach(() => { + rmSync(tmpDir, { recursive: true, force: true }); + }); + + const agents = [ + { module: "claude", configFile: ".mcp.json", hintFile: "CLAUDE.md" }, + { module: "cursor", configFile: ".cursor/mcp.json", hintFile: ".cursor/rules/stdlib.mdc" }, + { module: "copilot", configFile: ".vscode/mcp.json", hintFile: ".github/copilot-instructions.md" }, + { module: "windsurf", configFile: ".windsurf/mcp.json", hintFile: ".windsurf/rules/stdlib.md" }, + { module: "kiro", configFile: ".kiro/settings/mcp.json", hintFile: "AGENTS.md" }, + { module: "opencode", configFile: "opencode.json", hintFile: "AGENTS.md" } + ]; + + for (const { module: mod, configFile, hintFile } of agents) { + it(`should configure ${mod} agent`, async () => { + const agent = await import( + `../../src/mcp/features/Configure/agents/${mod}.js` + ); + await agent.default.init({ cwd: tmpDir }); + + expect(existsSync(join(tmpDir, configFile))).toBe(true); + expect(existsSync(join(tmpDir, hintFile))).toBe(true); + + const config = JSON.parse(readFileSync(join(tmpDir, configFile), "utf-8")); + const hint = readFileSync(join(tmpDir, hintFile), "utf-8"); + + expect(JSON.stringify(config)).toContain("stdlib"); + expect(hint).toContain("list_stdlib_skills"); + }); + } + + it("should configure cline agent (no hint file)", async () => { + const agent = await import( + "../../src/mcp/features/Configure/agents/cline.js" + ); + await agent.default.init({ cwd: tmpDir }); + + expect( + existsSync(join(tmpDir, ".vscode/cline_mcp_settings.json")) + ).toBe(true); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +```bash +yarn test -- __tests__/mcp/Configure.test.ts +``` + +- [ ] **Step 3: Create agents/types.ts** + +Create `src/mcp/features/Configure/agents/types.ts`: + +```ts +export interface AgentPreset { + slug: string; + displayName: string; + configFile: string; + configKey: string; + hintFile?: string; +} + +export interface AgentModule { + preset: AgentPreset; + init: (params: { cwd: string }) => Promise; +} +``` + +- [ ] **Step 4: Create agents/shared.ts** + +Create `src/mcp/features/Configure/agents/shared.ts`: + +```ts +import { + existsSync, + mkdirSync, + readFileSync, + writeFileSync +} from "node:fs"; +import { dirname, join } from "node:path"; + +interface WriteMcpConfigParams { + cwd: string; + configFile: string; + configKey: string; +} + +interface WriteHintFileParams { + cwd: string; + hintFile: string; + content: string; +} + +export function writeMcpConfig({ cwd, configFile, configKey }: WriteMcpConfigParams): void { + const configPath = join(cwd, configFile); + mkdirSync(dirname(configPath), { recursive: true }); + + let existing: Record = {}; + if (existsSync(configPath)) { + existing = JSON.parse(readFileSync(configPath, "utf-8")) as Record; + } + + const section = (existing[configKey] ?? {}) as Record; + if (section["stdlib"]) { + return; + } + + section["stdlib"] = { + command: "npx", + args: ["stdlib-mcp", "serve"] + }; + existing[configKey] = section; + + writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n"); +} + +export function writeHintFile({ cwd, hintFile, content }: WriteHintFileParams): void { + const hintPath = join(cwd, hintFile); + mkdirSync(dirname(hintPath), { recursive: true }); + + if (existsSync(hintPath)) { + const existing = readFileSync(hintPath, "utf-8"); + if (existing.includes("list_stdlib_skills")) { + return; + } + const separator = existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n"; + writeFileSync(hintPath, existing + separator + content + "\n"); + } else { + writeFileSync(hintPath, content + "\n"); + } +} + +export function stdlibHintBlock(): string { + return `## @webiny/stdlib MCP + +This project uses \`@webiny/stdlib\`. An MCP server is available with tools for discovering stdlib features: + +- \`list_stdlib_skills\` — returns a catalog of all available skills with names and descriptions. Call this first when working with @webiny/stdlib. +- \`get_stdlib_skill\` — loads full documentation for a specific skill by name.`; +} +``` + +- [ ] **Step 5: Create all 7 agent adapters** + +Create each agent file following this pattern. Showing `claude.ts` in full, others follow the same structure: + +`src/mcp/features/Configure/agents/claude.ts`: +```ts +import type { AgentModule } from "./types.js"; +import { writeMcpConfig, writeHintFile, stdlibHintBlock } from "./shared.js"; + +const module: AgentModule = { + preset: { + slug: "claude", + displayName: "Claude Code", + configFile: ".mcp.json", + configKey: "mcpServers", + hintFile: "CLAUDE.md" + }, + async init({ cwd }) { + writeMcpConfig({ cwd, configFile: ".mcp.json", configKey: "mcpServers" }); + writeHintFile({ cwd, hintFile: "CLAUDE.md", content: stdlibHintBlock() }); + } +}; + +export default module; +``` + +`src/mcp/features/Configure/agents/cursor.ts`: +```ts +import type { AgentModule } from "./types.js"; +import { writeMcpConfig, writeHintFile, stdlibHintBlock } from "./shared.js"; + +const module: AgentModule = { + preset: { + slug: "cursor", + displayName: "Cursor", + configFile: ".cursor/mcp.json", + configKey: "mcpServers", + hintFile: ".cursor/rules/stdlib.mdc" + }, + async init({ cwd }) { + writeMcpConfig({ cwd, configFile: ".cursor/mcp.json", configKey: "mcpServers" }); + writeHintFile({ cwd, hintFile: ".cursor/rules/stdlib.mdc", content: stdlibHintBlock() }); + } +}; + +export default module; +``` + +`src/mcp/features/Configure/agents/cline.ts`: +```ts +import type { AgentModule } from "./types.js"; +import { writeMcpConfig } from "./shared.js"; + +const module: AgentModule = { + preset: { + slug: "cline", + displayName: "Cline", + configFile: ".vscode/cline_mcp_settings.json", + configKey: "mcpServers" + }, + async init({ cwd }) { + writeMcpConfig({ + cwd, + configFile: ".vscode/cline_mcp_settings.json", + configKey: "mcpServers" + }); + } +}; + +export default module; +``` + +`src/mcp/features/Configure/agents/copilot.ts`: +```ts +import type { AgentModule } from "./types.js"; +import { writeMcpConfig, writeHintFile, stdlibHintBlock } from "./shared.js"; + +const module: AgentModule = { + preset: { + slug: "copilot", + displayName: "GitHub Copilot", + configFile: ".vscode/mcp.json", + configKey: "servers", + hintFile: ".github/copilot-instructions.md" + }, + async init({ cwd }) { + writeMcpConfig({ cwd, configFile: ".vscode/mcp.json", configKey: "servers" }); + writeHintFile({ + cwd, + hintFile: ".github/copilot-instructions.md", + content: stdlibHintBlock() + }); + } +}; + +export default module; +``` + +`src/mcp/features/Configure/agents/windsurf.ts`: +```ts +import type { AgentModule } from "./types.js"; +import { writeMcpConfig, writeHintFile, stdlibHintBlock } from "./shared.js"; + +const module: AgentModule = { + preset: { + slug: "windsurf", + displayName: "Windsurf", + configFile: ".windsurf/mcp.json", + configKey: "mcpServers", + hintFile: ".windsurf/rules/stdlib.md" + }, + async init({ cwd }) { + writeMcpConfig({ cwd, configFile: ".windsurf/mcp.json", configKey: "mcpServers" }); + writeHintFile({ + cwd, + hintFile: ".windsurf/rules/stdlib.md", + content: stdlibHintBlock() + }); + } +}; + +export default module; +``` + +`src/mcp/features/Configure/agents/kiro.ts`: +```ts +import type { AgentModule } from "./types.js"; +import { writeMcpConfig, writeHintFile, stdlibHintBlock } from "./shared.js"; + +const module: AgentModule = { + preset: { + slug: "kiro", + displayName: "Kiro", + configFile: ".kiro/settings/mcp.json", + configKey: "mcpServers", + hintFile: "AGENTS.md" + }, + async init({ cwd }) { + writeMcpConfig({ + cwd, + configFile: ".kiro/settings/mcp.json", + configKey: "mcpServers" + }); + writeHintFile({ cwd, hintFile: "AGENTS.md", content: stdlibHintBlock() }); + } +}; + +export default module; +``` + +`src/mcp/features/Configure/agents/opencode.ts`: +```ts +import { + existsSync, + mkdirSync, + readFileSync, + writeFileSync +} from "node:fs"; +import { dirname, join } from "node:path"; +import type { AgentModule } from "./types.js"; +import { writeHintFile, stdlibHintBlock } from "./shared.js"; + +const module: AgentModule = { + preset: { + slug: "opencode", + displayName: "OpenCode", + configFile: "opencode.json", + configKey: "mcp", + hintFile: "AGENTS.md" + }, + async init({ cwd }) { + const configPath = join(cwd, "opencode.json"); + mkdirSync(dirname(configPath), { recursive: true }); + + let existing: Record = {}; + if (existsSync(configPath)) { + existing = JSON.parse(readFileSync(configPath, "utf-8")) as Record; + } + + const section = (existing["mcp"] ?? {}) as Record; + if (section["stdlib"]) { + return; + } + + section["stdlib"] = { + type: "stdio", + command: ["npx", "stdlib-mcp", "serve"], + enabled: true + }; + existing["mcp"] = section; + writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n"); + + writeHintFile({ cwd, hintFile: "AGENTS.md", content: stdlibHintBlock() }); + } +}; + +export default module; +``` + +- [ ] **Step 6: Create AgentConfigurator abstraction** + +Create `src/mcp/features/Configure/abstractions/AgentConfigurator.ts`: + +```ts +import { createAbstraction } from "~/common/index.js"; + +export interface IAgentConfigurator { + configure(): Promise; +} + +export const AgentConfigurator = createAbstraction( + "Mcp/AgentConfigurator" +); + +export namespace AgentConfigurator { + export type Interface = IAgentConfigurator; +} +``` + +Create `src/mcp/features/Configure/abstractions/index.ts`: + +```ts +export { AgentConfigurator } from "./AgentConfigurator.js"; +``` + +- [ ] **Step 7: Create AgentConfigurator implementation** + +Create `src/mcp/features/Configure/AgentConfigurator.ts`: + +```ts +import { createInterface } from "node:readline"; +import { AgentConfigurator as AgentConfiguratorAbstraction } from "./abstractions/AgentConfigurator.js"; +import type { AgentModule } from "./agents/types.js"; +import claudeAgent from "./agents/claude.js"; +import cursorAgent from "./agents/cursor.js"; +import clineAgent from "./agents/cline.js"; +import copilotAgent from "./agents/copilot.js"; +import windsurfAgent from "./agents/windsurf.js"; +import kiroAgent from "./agents/kiro.js"; +import opencodeAgent from "./agents/opencode.js"; + +const AGENTS: AgentModule[] = [ + claudeAgent, + clineAgent, + copilotAgent, + cursorAgent, + kiroAgent, + opencodeAgent, + windsurfAgent +].sort((a, b) => a.preset.displayName.localeCompare(b.preset.displayName)); + +class AgentConfiguratorImpl implements AgentConfiguratorAbstraction.Interface { + public async configure(): Promise { + console.log("\nSelect your AI agent:\n"); + for (let i = 0; i < AGENTS.length; i++) { + console.log(` ${i + 1}. ${AGENTS[i]!.preset.displayName}`); + } + console.log(); + + const rl = createInterface({ input: process.stdin, output: process.stdout }); + + const answer = await new Promise(resolve => { + rl.question("Enter number: ", resolve); + }); + rl.close(); + + const index = parseInt(answer, 10) - 1; + if (isNaN(index) || index < 0 || index >= AGENTS.length) { + console.error("Invalid selection."); + return; + } + + const agent = AGENTS[index]!; + console.log(`\nConfiguring ${agent.preset.displayName}...`); + await agent.init({ cwd: process.cwd() }); + console.log("Done."); + } +} + +export const AgentConfigurator = AgentConfiguratorAbstraction.createImplementation({ + implementation: AgentConfiguratorImpl, + dependencies: [] +}); +``` + +- [ ] **Step 8: Create feature.ts and index.ts** + +Create `src/mcp/features/Configure/feature.ts`: + +```ts +import { createFeature } from "~/common/index.js"; +import { AgentConfigurator } from "./AgentConfigurator.js"; + +export const AgentConfiguratorFeature = createFeature({ + name: "Mcp/AgentConfiguratorFeature", + register(container) { + container.register(AgentConfigurator).inSingletonScope(); + } +}); +``` + +Create `src/mcp/features/Configure/index.ts`: + +```ts +export { AgentConfigurator } from "./abstractions/index.js"; +export { AgentConfiguratorFeature } from "./feature.js"; +export type { AgentPreset, AgentModule } from "./agents/types.js"; +export { writeMcpConfig, writeHintFile, stdlibHintBlock } from "./agents/shared.js"; +``` + +- [ ] **Step 9: Create README.md** + +Create `src/mcp/features/Configure/README.md`: + +```markdown +# Configure + +Interactive CLI that configures AI agents to use the stdlib MCP server. Supports Claude Code, Cursor, Cline, GitHub Copilot, Windsurf, Kiro, and OpenCode. + +## Interface + +- `configure(): Promise` — presents agent selection menu, writes MCP config and hint files for the chosen agent. + +## Usage + +```ts +import { Container } from "@webiny/di"; +import { AgentConfiguratorFeature, AgentConfigurator } from "@webiny/stdlib/mcp"; + +const container = new Container(); +AgentConfiguratorFeature.register(container); + +await container.resolve(AgentConfigurator).configure(); +``` +``` + +- [ ] **Step 10: Update mcp barrel** + +Update `src/mcp/index.ts`: + +```ts +export { + SkillDiscovery, + SkillDiscoveryConfig, + McpServer, + McpServerFeature, + type Skill +} from "./features/Server/index.js"; + +export { + AgentConfigurator, + AgentConfiguratorFeature, + type AgentPreset, + type AgentModule, + writeMcpConfig, + writeHintFile, + stdlibHintBlock +} from "./features/Configure/index.js"; +``` + +- [ ] **Step 11: Run tests** + +```bash +yarn test -- __tests__/mcp/Configure.test.ts +``` + +Expected: all tests PASS. + +- [ ] **Step 12: Run typecheck** + +```bash +yarn typecheck +``` + +- [ ] **Step 13: Commit** + +```bash +git add -A +git commit -m "feat(mcp): add Configure feature with 7 agent adapters + +Interactive CLI for configuring Claude, Cursor, Cline, Copilot, +Windsurf, Kiro, and OpenCode. Writes MCP config and hint files." +``` + +--- + +### Task 5: CLI Entry Point + +**Files:** +- Create: `src/mcp/cli.ts` + +**Interfaces:** +- Consumes: `SkillDiscoveryConfig`, `McpServerFeature`, `McpServer`, `AgentConfiguratorFeature`, `AgentConfigurator` from Tasks 2-4 +- Produces: `stdlib-mcp` binary entry point + +- [ ] **Step 1: Create CLI entry point** + +Create `src/mcp/cli.ts`: + +```ts +#!/usr/bin/env node +import { existsSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { Container } from "@webiny/di"; +import { SkillDiscoveryConfig, McpServerFeature, McpServer } from "./features/Server/index.js"; +import { AgentConfiguratorFeature, AgentConfigurator } from "./features/Configure/index.js"; + +function parseFlags(argv: string[]): { skills: string[]; additionalSkills: string[] } { + const skills: string[] = []; + const additionalSkills: string[] = []; + + for (const arg of argv) { + if (arg.startsWith("--skills=")) { + skills.push(arg.slice("--skills=".length)); + } else if (arg.startsWith("--additional-skills=")) { + additionalSkills.push(arg.slice("--additional-skills=".length)); + } + } + + return { skills, additionalSkills }; +} + +function resolvePackageRoot(): string { + const thisFile = fileURLToPath(import.meta.url); + let dir = dirname(thisFile); + while (dir !== dirname(dir)) { + if (existsSync(join(dir, "package.json"))) { + return dir; + } + dir = dirname(dir); + } + return dirname(thisFile); +} + +function resolveDefaultScanPaths(packageRoot: string): string[] { + const paths: string[] = []; + + const skillsDir = join(packageRoot, "skills"); + if (existsSync(skillsDir)) { + paths.push(skillsDir); + } + + const srcDir = join(packageRoot, "src"); + const distDir = join(packageRoot, "dist"); + if (existsSync(srcDir)) { + paths.push(srcDir); + } else if (existsSync(distDir)) { + paths.push(distDir); + } + + return paths; +} + +const command = process.argv[2]; + +if (command === "serve") { + const flags = parseFlags(process.argv.slice(3)); + const packageRoot = resolvePackageRoot(); + + let scanPaths: string[]; + if (flags.skills.length > 0) { + scanPaths = [...flags.additionalSkills, ...flags.skills]; + } else { + scanPaths = [...flags.additionalSkills, ...resolveDefaultScanPaths(packageRoot)]; + } + + const container = new Container(); + container.registerInstance(SkillDiscoveryConfig, { scanPaths }); + McpServerFeature.register(container); + await container.resolve(McpServer).start(); +} else if (command === "configure") { + const container = new Container(); + AgentConfiguratorFeature.register(container); + await container.resolve(AgentConfigurator).configure(); +} else { + console.error("Usage: stdlib-mcp "); + process.exit(1); +} +``` + +- [ ] **Step 2: Build and verify shebang** + +```bash +rm -rf dist && yarn build +head -1 dist/mcp/cli.js +``` + +Expected: `#!/usr/bin/env node` + +- [ ] **Step 3: Run typecheck and full tests** + +```bash +yarn typecheck && yarn test +``` + +- [ ] **Step 4: Commit** + +```bash +git add -A +git commit -m "feat(mcp): add CLI entry point for serve and configure + +Dispatches stdlib-mcp serve (starts MCP server) and stdlib-mcp +configure (interactive agent setup). Resolves default scan paths +from package root with src/dist fallback." +``` + +--- + +### Task 6: Add Front-matter to Feature READMEs + Create Skills + +**Files:** +- Modify: all 22 `src/**/README.md` files (add YAML front-matter) +- Create: `skills/di-patterns/SKILL.md` +- Create: `skills/adding-a-feature/SKILL.md` +- Create: `skills/testing-patterns/SKILL.md` + +**Interfaces:** +- Consumes: nothing +- Produces: discoverable skills for the MCP server + +- [ ] **Step 1: Add front-matter to all 22 feature READMEs** + +For each README, prepend YAML front-matter. Read each file first, then prepend the block. The name, description, and context for each are defined in the spec's "Skills Content" table. + +Example for `src/node/features/FileTool/README.md` — prepend: +```yaml +--- +name: file-tool +description: Read, write, copy, remove files. All paths must be absolute. +context: node +--- + +``` + +Do this for all 22 READMEs listed in the spec. Read each README to derive an accurate one-line description from its content. + +- [ ] **Step 2: Create skills/ directory and cross-cutting skills** + +Create `skills/di-patterns/SKILL.md`: +```yaml +--- +name: di-patterns +description: How dependency injection works in @webiny/stdlib — abstractions, implementations, features, and container wiring. +context: guides +--- +``` +Then write content covering: `createAbstraction`, `createImplementation`, `createFeature`, `Container.register/resolve`, optional dependencies, singleton scope. + +Create `skills/adding-a-feature/SKILL.md`: +```yaml +--- +name: adding-a-feature +description: Step-by-step guide for adding a new tool or service to @webiny/stdlib. +context: guides +--- +``` +Content mirrors AGENTS.md "Step-by-step: Adding a New Tool" section. + +Create `skills/testing-patterns/SKILL.md`: +```yaml +--- +name: testing-patterns +description: Testing conventions for @webiny/stdlib — container setup, tmpdir patterns, browser environment directives. +context: guides +--- +``` +Content covers `makeContainer()`, tmpdir cleanup, `@vitest-environment happy-dom` directive, coverage. + +- [ ] **Step 3: Verify skills are discoverable** + +```bash +rm -rf dist && yarn build +node -e " +import { Container } from '@webiny/di'; +import { SkillDiscoveryConfig, McpServerFeature, SkillDiscovery } from './dist/mcp/index.js'; +const c = new Container(); +c.registerInstance(SkillDiscoveryConfig, { scanPaths: ['./skills', './src'] }); +McpServerFeature.register(c); +const skills = c.resolve(SkillDiscovery).discover(); +console.log(skills.length + ' skills found'); +skills.forEach(s => console.log(' ' + s.name + ' (' + s.context + ')')); +" +``` + +Expected: 25 skills (22 READMEs + 3 cross-cutting). + +- [ ] **Step 4: Run full pre-commit chain** + +```bash +yarn && yarn adio && yarn format:fix && yarn lint:fix && yarn typecheck && yarn build && yarn test:coverage +``` + +- [ ] **Step 5: Commit** + +```bash +git add -A +git commit -m "feat(mcp): add front-matter to feature READMEs and create cross-cutting skills + +All 22 feature READMEs now have YAML front-matter for MCP discovery. +Three cross-cutting skill files added: di-patterns, adding-a-feature, +testing-patterns." +``` + +--- + +### Task 7: Final Integration and E2E Verification + +**Files:** +- No new files — verification only + +**Interfaces:** +- Consumes: everything from Tasks 1-6 + +- [ ] **Step 1: Run full pre-commit chain** + +```bash +yarn && yarn adio && yarn format:fix && yarn lint:fix && yarn typecheck && yarn build && yarn test:coverage +``` + +All steps must pass with zero errors. + +- [ ] **Step 2: Verify dist structure** + +```bash +ls dist/mcp/cli.js +head -1 dist/mcp/cli.js +find dist -name "README.md" | wc -l +ls dist/mcp/features/Server/ +ls dist/mcp/features/Configure/ +``` + +- [ ] **Step 3: Verify npm pack includes skills/** + +```bash +cd dist && npm pack --dry-run 2>&1 | grep -E "skills/|SKILL.md" +``` + +Wait — skills/ is at repo root, not in dist/. Verify it's included via the root package.json `files` field: + +```bash +cd .. && npm pack --dry-run 2>&1 | head -20 +``` + +- [ ] **Step 4: Manual E2E — serve command** + +```bash +npx stdlib-mcp serve & +# In another terminal, use MCP inspector: +# npx @modelcontextprotocol/inspector +``` + +Or test directly: +```bash +echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}' | npx stdlib-mcp serve 2>/dev/null +``` + +- [ ] **Step 5: Manual E2E — configure command** + +```bash +cd /tmp && mkdir test-configure && cd test-configure +npx stdlib-mcp configure +# Select "Claude Code" +# Verify .mcp.json and CLAUDE.md created +cat .mcp.json +cat CLAUDE.md +cd .. && rm -rf test-configure +``` + +- [ ] **Step 6: Final commit if any fixes were needed** + +```bash +git status +# If changes exist: +git add -A +git commit -m "fix(mcp): integration fixes from E2E verification" +``` diff --git a/docs/superpowers/specs/2026-07-28-mcp-server-design.md b/docs/superpowers/specs/2026-07-28-mcp-server-design.md new file mode 100644 index 0000000..944897f --- /dev/null +++ b/docs/superpowers/specs/2026-07-28-mcp-server-design.md @@ -0,0 +1,427 @@ +# MCP Server for @webiny/stdlib + +## Summary + +Add an MCP server to `@webiny/stdlib` that helps AI agents discover and learn about available stdlib tools and features. Two MCP tools (`list_stdlib_skills` and `get_stdlib_skill`) provide a lazy-loading pattern where agents only load documentation they need for the current task. + +## Decisions + +- **Location:** New `src/mcp/` slice alongside `src/common/`, `src/node/`, `src/browser/`. This is dev infrastructure, not a user-facing library — it sits at the same level as other slices but serves a different purpose. +- **Architecture:** DI for core pieces (SkillDiscovery, McpServer, AgentConfigurator) using `features//` layout for consistency with repo conventions. Plain modules for agent adapters. +- **Skill source:** Front-matter added to existing feature READMEs + standalone SKILL.md files in `skills/` for cross-cutting topics. +- **Dependencies:** `@modelcontextprotocol/sdk` `^1.30.0`, `@11ty/gray-matter` `^2.1.0` as regular dependencies of `@webiny/stdlib`. Both are pure ESM, no peer dependency conflicts. +- **CLI:** `stdlib-mcp serve` and `stdlib-mcp configure` commands. +- **Agent support:** All 7 agents (Claude, Cursor, Cline, Copilot, Windsurf, Kiro, OpenCode). +- **Platform:** Node-specific slice. Uses `node:fs`, `node:path`, `node:readline`. tsconfig sets `types: ["node"]`. +- **Cross-slice imports:** The mcp slice uses Node built-ins directly (not stdlib's FileTool/DirectoryTool). It imports from `~/common/index.js` only for `createAbstraction` and `createFeature`. + +## Source Layout + +``` +src/mcp/ +├── index.ts # barrel exports +├── cli.ts # CLI entry with shebang +├── features/ +│ ├── Server/ +│ │ ├── abstractions/ +│ │ │ ├── SkillDiscovery.ts # ISkillDiscovery + token +│ │ │ ├── McpServer.ts # IMcpServer + token +│ │ │ └── index.ts +│ │ ├── SkillDiscovery.ts # impl: scan, parse @11ty/gray-matter, build catalog +│ │ ├── McpServer.ts # impl: register tools, stdio transport +│ │ ├── feature.ts # McpServerFeature +│ │ ├── README.md +│ │ └── index.ts +│ └── Configure/ +│ ├── abstractions/ +│ │ ├── AgentConfigurator.ts # IAgentConfigurator + token +│ │ └── index.ts +│ ├── AgentConfigurator.ts # impl: list agents, prompt, run init +│ ├── agents/ # plain modules, no DI +│ │ ├── types.ts # AgentPreset, AgentModule interfaces +│ │ ├── shared.ts # writeMcpConfig, writeHintFile helpers +│ │ ├── claude.ts +│ │ ├── cursor.ts +│ │ ├── cline.ts +│ │ ├── copilot.ts +│ │ ├── windsurf.ts +│ │ ├── kiro.ts +│ │ └── opencode.ts +│ ├── feature.ts # AgentConfiguratorFeature +│ ├── README.md +│ └── index.ts +skills/ # cross-cutting skill files (repo root) +``` + +## MCP Tools + +### `list_stdlib_skills` + +- **Input:** none +- **Output:** Markdown catalog grouped by `context` field. Each context section has a heading and a table of `| Skill | Description |`. Skills sorted alphabetically within each group. +- **Annotations:** `{ readOnlyHint: true }` + +Context group headings (rendered in this order): +- `common` — "Platform-agnostic utilities" +- `node` — "Node.js-specific tools" +- `browser` — "Browser-specific tools" +- `guides` — "Cross-cutting guides and patterns" + +Any `context` value not in this list gets its own section at the end with the raw context value as heading. + +### `get_stdlib_skill` + +- **Input:** `{ topic: z.string().describe("Skill name from list_stdlib_skills") }` (zod is already a dependency of `@webiny/stdlib`) +- **Output:** Full markdown body (@11ty/gray-matter stripped via `matter(raw).content`, then `.trim()`). +- **Error response:** When skill name not found, return `{ isError: true, content: [{ type: "text", text: "Skill \"\" not found. Available skills:\n- skill-a (common)\n- skill-b (node)\n..." }] }`. Lists all skills sorted alphabetically with context label. +- **Annotations:** `{ readOnlyHint: true }` + +## Skill Discovery + +### Sources and default scan paths + +When `stdlib-mcp serve` runs, it resolves default skill paths relative to the **package root** (the directory containing `package.json`, resolved via `import.meta.url` walking up from `cli.js`): + +1. **Standalone skills** — `${packageRoot}/skills/**/*.md` files with valid @11ty/gray-matter. Scanned first (higher priority). +2. **Feature READMEs** — `${packageRoot}/src/**/README.md` files with valid @11ty/gray-matter. Scanned second. + +**Development vs installed resolution:** The server always tries `src/` first. If `src/` does not exist (npm-installed package), it falls back to `dist/`. This is unambiguous — there is no priority conflict because only one of the two directories exists in any given installation. During development `src/` exists and is used; in `node_modules/` only `dist/` exists. + +The `skills/` directory must also be included in the published package. Add `"skills"` to the `files` array in `package.json` alongside `"dist"`. + +**Missing directories:** If a scan root directory does not exist (`readdirSync` throws `ENOENT`), skip it silently and continue with remaining paths. This handles: empty projects, missing `skills/`, missing `src/` (before fallback to `dist/`). + +### Scan precedence with flags + +Full scan order (first match wins on name collision): + +1. `--additional-skills=` directories (if provided; multiple allowed, scanned in argument order) +2. `--skills=` directories (if provided; replaces defaults below entirely) +3. Default: `${packageRoot}/skills/` then `${packageRoot}/src/` (or `dist/` if `src/` absent) + +`--skills` and `--additional-skills` can both be used: additional-skills always come first regardless. + +### Front-matter format + +```yaml +--- +name: file-tool +description: Read, write, copy, remove files. All paths must be absolute. +context: node +--- +``` + +Required: `name` (non-empty string), `description` (non-empty string). +Optional: `context` (string, defaults to `"common"`). + +Validation: empty strings for `name` or `description` are treated as missing — skip with `console.warn`. + +### Error handling + +- Files with no YAML @11ty/gray-matter: **skip silently**. Not every README needs @11ty/gray-matter — only those intended as skills. +- Files with @11ty/gray-matter but missing `name` or `description`: **skip with `console.warn`** noting the file path and missing field. +- Files with non-string `name`/`description`/`context`: **skip with `console.warn`**. +- Invalid YAML syntax in @11ty/gray-matter: **skip with `console.warn`** noting parse error. +- Unreadable files (permissions): **skip with `console.warn`**. + +### Name collision + +First match wins based on scan order. No logging for collisions — first match wins silently. + +### Skill type + +```ts +interface Skill { + name: string; + description: string; + context: string; + body: string; // markdown content after @11ty/gray-matter stripping, trimmed +} +``` + +## DI Structure + +Token namespace: `"Mcp/"` — a new domain prefix for MCP infrastructure, distinct from `"Core/"` (platform-agnostic) and `"Node/"` (Node tools). + +### SkillDiscoveryConfig + +- **Abstraction:** `ISkillDiscoveryConfig` — `{ scanPaths: string[] }` +- **Token:** `"Mcp/SkillDiscoveryConfig"` +- **Registered as instance** in the CLI entry point after parsing flags. The CLI resolves default paths (package root + flag overrides) and registers the config before resolving McpServer. + +### SkillDiscovery + +- **Abstraction:** `ISkillDiscovery` + - `discover(): Skill[]` — scans paths from injected config, returns all valid skills +- **Token:** `"Mcp/SkillDiscovery"` +- **Dependencies:** `[SkillDiscoveryConfig]` +- **Implementation:** Reads `config.scanPaths`, walks each directory recursively with `readdirSync` (skips missing directories silently), reads markdown files, parses @11ty/gray-matter with `@11ty/gray-matter` library (npm package `@11ty/gray-matter`). + +### McpServer + +- **Abstraction:** `IMcpServer` + - `start(): Promise` — registers tools, connects stdio transport, blocks until stdin closes. The MCP SDK handles the event loop — `start()` resolves only when the transport disconnects (stdin EOF or process signal). Callers should `await start()` and then exit. +- **Token:** `"Mcp/McpServer"` +- **Dependencies:** `[SkillDiscovery]` +- **Implementation:** Calls `skillDiscovery.discover()` in `start()` to build the skill cache (throws if discovery fails — propagates to CLI). Creates `McpServer` from `@modelcontextprotocol/sdk/server/mcp.js`, registers both tools, connects `StdioServerTransport`. The MCP SDK manages stdin/stdout lifecycle and handles unexpected disconnects. Any error from `discover()` or `connect()` rejects the `start()` promise. + +### AgentConfigurator + +- **Abstraction:** `IAgentConfigurator` + - `configure(): Promise` — presents agent list on stdout, reads selection from stdin via `node:readline`, runs the selected agent's `init` function. Returns after configuration completes. If `init()` throws, the error propagates to the CLI (which crashes with a stack trace — standard Node behavior). +- **Token:** `"Mcp/AgentConfigurator"` +- **Dependencies:** none +- **Implementation:** Imports all agent modules statically. Collects their `preset` objects, sorts by `displayName` alphabetically, and builds the selection menu. On selection, calls the agent's `init({ cwd: process.cwd() })` function. + +## Agent Adapters + +### AgentPreset type + +```ts +interface AgentPreset { + slug: string; // lowercase alphanumeric + hyphens, e.g., "claude" + displayName: string; // human-readable, e.g., "Claude Code" + configFile: string; // relative path from cwd, may be nested, e.g., ".cursor/mcp.json" + configKey: string; // top-level key in config JSON, e.g., "mcpServers" + hintFile?: string; // relative path from cwd, e.g., "CLAUDE.md". Omitted when agent requires manual hint setup. +} + +interface AgentModule { + preset: AgentPreset; + init: (params: { cwd: string }) => Promise; +} +``` + +### Agent table + +| Agent | Slug | Config file | Config key | Hint file | +|-----------|------------|------------------------------------|----------------|----------------------------------| +| Claude | `claude` | `.mcp.json` | `mcpServers` | `CLAUDE.md` | +| Cursor | `cursor` | `.cursor/mcp.json` | `mcpServers` | `.cursor/rules/stdlib.mdc` | +| Cline | `cline` | `.vscode/cline_mcp_settings.json` | `mcpServers` | _(none — manual)_ | +| Copilot | `copilot` | `.vscode/mcp.json` | `servers` | `.github/copilot-instructions.md`| +| Windsurf | `windsurf` | `.windsurf/mcp.json` | `mcpServers` | `.windsurf/rules/stdlib.md` | +| Kiro | `kiro` | `.kiro/settings/mcp.json` | `mcpServers` | `AGENTS.md` | +| OpenCode | `opencode` | `opencode.json` | `mcp` | `AGENTS.md` | + +Note: Cursor requires `.mdc` extension for rule files — this is a Cursor-specific convention, not standard markdown. + +### Shared helpers + +- **`writeMcpConfig({ cwd, configFile, configKey })`** — reads existing JSON (or `{}` if file missing), merges `stdlib` server entry under `configKey`, writes back with 2-space indent. Creates parent directories with `mkdirSync({ recursive: true })`. Skips if `stdlib` entry already exists under `configKey`. Throws on write failure (permissions, disk) — error propagates to CLI. +- **`writeHintFile({ cwd, hintFile, content })`** — appends hint block to markdown file. Creates file if missing. Uses marker string (`"list_stdlib_skills"`) to detect if already present — skips if found. Creates parent directories with `mkdirSync({ recursive: true })`. Ensures two trailing newlines before the appended block (reads existing content, appends `\n\n` + content if file doesn't end with blank line). +- **`stdlibHintBlock()`** — returns instruction text: + +```markdown +## @webiny/stdlib MCP + +This project uses `@webiny/stdlib`. An MCP server is available with tools for discovering stdlib features: + +- `list_stdlib_skills` — returns a catalog of all available skills with names and descriptions. Call this first when working with @webiny/stdlib. +- `get_stdlib_skill` — loads full documentation for a specific skill by name. +``` + +### MCP config entry + +Standard shape (most agents): +```json +{ + "mcpServers": { + "stdlib": { + "command": "npx", + "args": ["stdlib-mcp", "serve"] + } + } +} +``` + +OpenCode uses a different shape: +```json +{ + "mcp": { + "stdlib": { + "type": "stdio", + "command": ["npx", "stdlib-mcp", "serve"], + "enabled": true + } + } +} +``` + +## Build Integration + +### Existing tsconfig.common.json change + +Add `"../src/mcp"` to the `exclude` array so the common slice does not compile mcp code: + +```json +"exclude": ["../src/node", "../src/browser", "../src/mcp"] +``` + +### New tsconfig files + +**`config/tsconfig.mcp.json`** (build): +```json +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "composite": true, + "rootDir": "../src/mcp", + "outDir": "../dist/mcp", + "module": "nodenext", + "moduleResolution": "nodenext", + "types": ["node"], + "paths": { "~/*": ["../src/*"] } + }, + "include": ["../src/mcp"], + "references": [{ "path": "./tsconfig.common.json" }] +} +``` + +**`config/tsconfig.check.mcp.json`** (type-check): +```json +{ + "extends": ["./tsconfig.mcp.json", "./tsconfig.checkmode.json"], + "include": ["../src/mcp", "../__tests__/mcp"] +} +``` + +### Root tsconfig.json + +Add reference after browser: `{ "path": "./config/tsconfig.mcp.json" }`. Order: common, node, browser, mcp. + +### package.json changes + +- New subpath export: `"./mcp": { "import": "./dist/mcp/index.js", "types": "./dist/mcp/index.d.ts" }` +- New `bin` field (fresh addition — no existing `bin` in package.json): `"bin": { "stdlib-mcp": "./dist/mcp/cli.js" }` +- `typecheck` script: append `&& tsgo -p config/tsconfig.check.mcp.json` +- `files` array: `["dist", "skills"]` — skills directory must be published for npm consumers +- Dependencies: add `@modelcontextprotocol/sdk` `^1.30.0`, `@11ty/gray-matter` `^2.1.0` + +### Build script + +Update `scripts/features/BuildPackages/index.ts` slices array — append `"config/tsconfig.mcp.json"` after browser (last position). Build order: common, node, browser, mcp. + +### CLI shebang + +The `src/mcp/cli.ts` source file includes `#!/usr/bin/env node` as the first line. **Implementation step 1 (build integration) must verify** that tsgo preserves the shebang in `dist/mcp/cli.js` by running `head -1 dist/mcp/cli.js` after the first build. If tsgo strips it, add a `ensureShebang(distDir)` method to `BuildOrchestrator` that prepends `#!/usr/bin/env node\n` to `dist/mcp/cli.js` after `pathAliasRewriter.rewrite()` and before `artifactCopier.copy*()`. + +npm automatically sets executable permissions on files referenced by the `bin` field during install/publish. No manual chmod needed. + +### README.md files in dist + +tsgo only emits `.js`, `.d.ts`, and `.map` files — it does NOT copy non-TS files like `README.md`. The build must copy all `README.md` files from `src/` into `dist/` so the server can discover them when installed as an npm package. Add a `copyReadmes(srcDir, distDir)` method to `BuildOrchestrator` that copies all `src/**/README.md` files to corresponding `dist/` paths (no @11ty/gray-matter filtering at build time — copy all, let the server filter at runtime). Run after compilation. This must be implemented in step 1 (build integration), not deferred. + +## CLI Entry Point + +`src/mcp/cli.ts`: + +```ts +#!/usr/bin/env node +import { Container } from "@webiny/di"; +// ... imports + +const command = process.argv[2]; + +if (command === "serve") { + const container = new Container(); + // register McpServerFeature, resolve McpServer, await start() + // start() blocks until stdin closes + // process exits naturally after start() resolves +} else if (command === "configure") { + const container = new Container(); + // register AgentConfiguratorFeature, resolve AgentConfigurator, await configure() +} else { + console.error("Usage: stdlib-mcp "); + process.exit(1); +} +``` + +Top-level `await` in an ESM module. Unhandled rejections from `start()` or `configure()` crash the process with a stack trace (Node default behavior). No custom signal handlers needed — the MCP SDK handles SIGTERM/SIGINT for stdio transport cleanup. + +## Testing + +### SkillDiscovery tests (`__tests__/mcp/SkillDiscovery.test.ts`) + +- Valid @11ty/gray-matter parsing (name, description, context extracted correctly) +- Default context value when omitted +- Missing @11ty/gray-matter: file skipped, no error +- Front-matter with missing `name`: skipped with warning +- Front-matter with missing `description`: skipped with warning +- Invalid YAML: skipped with warning +- Non-string field values: skipped with warning +- Directory scanning: finds README.md and SKILL.md recursively +- Name collision: first-scanned source wins +- Empty directory: returns empty array +- `body` field contains trimmed markdown content after @11ty/gray-matter stripping + +### McpServer tests (`__tests__/mcp/McpServer.test.ts`) + +- `list_stdlib_skills` returns markdown catalog grouped by context +- `list_stdlib_skills` with no skills returns `"No skills found."` text +- `list_stdlib_skills` skills sorted alphabetically within groups +- `get_stdlib_skill` returns trimmed body for valid skill name +- `get_stdlib_skill` returns error with available names for invalid name +- Tool registration (both tools registered with correct schemas and annotations) + +Transport mocking: use `@modelcontextprotocol/sdk`'s in-memory transport or mock the `McpServer` class directly. Avoid stdio in tests. + +### Agent adapter tests (`__tests__/mcp/Configure.test.ts`) + +- `writeMcpConfig`: creates new file with correct structure, patches existing file preserving other entries, skips if stdlib entry exists, creates parent directories +- `writeHintFile`: creates new file, appends to existing, skips if marker present, creates parent directories +- Each agent's `init` writes correct config file path and hint file path +- OpenCode adapter writes different JSON shape (`type`, `command` array, `enabled`) +- Copilot adapter uses `servers` key instead of `mcpServers` + +All tests use `tmpdir()` with cleanup in `afterEach`, following stdlib testing conventions. + +## Skills Content + +### Feature READMEs to update (all 22) + +Add @11ty/gray-matter to each existing README.md: + +| Feature | name | context | +|---------|------|---------| +| Cache | `cache` | `common` | +| Env | `env` | `common` | +| Logger | `logger` | `common` | +| boolean utils | `boolean` | `common` | +| dotProp utils | `dot-prop` | `common` | +| generateId utils | `generate-id` | `common` | +| mdbid utils | `mdbid` | `common` | +| uuid utils | `uuid` | `common` | +| utils (parent) | `utils` | `common` | +| DirectoryTool | `directory-tool` | `node` | +| FileTool | `file-tool` | `node` | +| HashFolderTool | `hash-folder-tool` | `node` | +| JsonFileTool | `json-file-tool` | `node` | +| NdJsonReaderTool | `ndjson-reader-tool` | `node` | +| PackageJsonFileTool | `package-json-file-tool` | `node` | +| PathTool | `path-tool` | `node` | +| PinoLogger | `pino-logger` | `node` | +| ProcessEnv | `process-env` | `node` | +| ReadStreamFactory | `read-stream-factory` | `node` | +| WorkspaceTool | `workspace-tool` | `node` | +| BrowserEnv | `browser-env` | `browser` | +| LocalStorageCache | `local-storage-cache` | `browser` | + +### Cross-cutting skills to create in `skills/` + +- `skills/di-patterns/SKILL.md` — DI system overview: abstractions, implementations, features, container wiring, optional dependencies +- `skills/adding-a-feature/SKILL.md` — step-by-step guide for adding a new tool (mirrors AGENTS.md section) +- `skills/testing-patterns/SKILL.md` — container setup, tmpdir patterns, browser env directives + +## Rollout + +1. Build integration: tsconfigs, package.json exports/bin/deps/files, build script slice, tsconfig.common.json exclude, README copy step, shebang verification. Run `yarn build` and confirm: `dist/mcp/cli.js` starts with shebang, `dist/**/README.md` files exist. +2. Implement SkillDiscoveryConfig + SkillDiscovery feature (abstraction + implementation + feature + README + tests) +3. Implement McpServer feature (abstraction + implementation + feature + README + tests) +4. Implement Configure feature with all 7 agent adapters (+ README + tests) +5. Add CLI entry point (wires DI, parses flags, registers SkillDiscoveryConfig) +6. Add @11ty/gray-matter to all 22 existing feature READMEs +7. Create 3 cross-cutting skill files in `skills/` +8. Run full pre-commit chain: `yarn && yarn adio && yarn format:fix && yarn lint:fix && yarn typecheck && yarn build && yarn test:coverage` +9. Manual E2E: run `npx stdlib-mcp serve`, connect with MCP inspector, call `list_stdlib_skills` (expect grouped catalog), call `get_stdlib_skill` with valid name (expect body), call with invalid name (expect error with list) +10. Manual E2E: run `npx stdlib-mcp configure`, select Claude, verify `.mcp.json` has stdlib entry, verify `CLAUDE.md` has hint block diff --git a/package.json b/package.json index 9c3af9b..371cc22 100644 --- a/package.json +++ b/package.json @@ -16,17 +16,30 @@ "./browser": { "import": "./dist/browser/index.js", "types": "./dist/browser/index.d.ts" + }, + "./mcp": { + "import": "./dist/mcp/index.js", + "types": "./dist/mcp/index.d.ts" } }, "main": "./dist/index.js", "types": "./dist/index.d.ts", + "bin": { + "stdlib-mcp": "./dist/mcp/cli.js" + }, "files": [ - "dist" + "dist", + "skills" ], + "publishConfig": { + "directory": "dist" + }, "dependencies": { + "@11ty/gray-matter": "^2.1.0", + "@modelcontextprotocol/sdk": "^1.29.0", "@webiny/di": "^1.0.2", "bson-objectid": "^2.0.4", - "dot-prop": "^10.1.0", + "dot-prop": "^10.2.0", "fast-glob": "^3.3.3", "nanoid": "^6.0.0", "nanoid-dictionary": "^5.0.0", @@ -40,9 +53,9 @@ "@types/node": "^26.1.1", "@vitest/coverage-v8": "^4.1.10", "adio": "^3.0.1", - "happy-dom": "^20.11.0", - "oxfmt": "^0.59.0", - "oxlint": "^1.74.0", + "happy-dom": "^20.11.1", + "oxfmt": "^0.60.0", + "oxlint": "^1.75.0", "typescript": "^7.0.2", "vitest": "^4.1.10" }, @@ -59,7 +72,7 @@ "lint": "oxlint --deny-warnings", "lint:fix": "oxlint --fix", "check:imports": "adio", - "typecheck": "tsc -p config/tsconfig.check.common.json && tsc -p config/tsconfig.check.node.json && tsc -p config/tsconfig.check.browser.json && tsc -p config/tsconfig.check.scripts.json", + "typecheck": "tsc -p config/tsconfig.check.common.json && tsc -p config/tsconfig.check.node.json && tsc -p config/tsconfig.check.browser.json && tsc -p config/tsconfig.check.scripts.json && tsc -p config/tsconfig.check.mcp.json", "full": "yarn check:imports && yarn clean && yarn format:fix && yarn lint:fix && yarn typecheck && yarn build && yarn test:coverage" }, "packageManager": "yarn@4.17.1" diff --git a/scripts/features/BuildPackages/ArtifactCopier.ts b/scripts/features/BuildPackages/ArtifactCopier.ts index f91a3b3..b408449 100644 --- a/scripts/features/BuildPackages/ArtifactCopier.ts +++ b/scripts/features/BuildPackages/ArtifactCopier.ts @@ -1,30 +1,24 @@ import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { ArtifactCopier as ArtifactCopierAbstraction } from "./abstractions/ArtifactCopier.ts"; - -interface PackageJsonExports { - [key: string]: string | PackageJsonExports; -} - -interface PackageJson { - main?: string; - types?: string; - exports?: string | PackageJsonExports; - files?: string[]; - [key: string]: unknown; -} +import type { PackageJson } from "type-fest"; function stripDist(path: string): string { return path.startsWith("./dist/") ? `./${path.slice("./dist/".length)}` : path; } -function rewriteExports(value: string | PackageJsonExports): string | PackageJsonExports { +function rewriteExports(value: PackageJson.Exports): PackageJson.Exports { + if (value == null || Array.isArray(value)) { + return value; + } if (typeof value === "string") { return stripDist(value); } - const result: PackageJsonExports = {}; + const result: PackageJson.ExportConditions = {}; for (const [k, v] of Object.entries(value)) { - result[k] = rewriteExports(v); + if (v !== undefined) { + result[k] = rewriteExports(v); + } } return result; } @@ -36,16 +30,17 @@ class ArtifactCopierImpl implements ArtifactCopierAbstraction.Interface { readFileSync(join(packageAbsDir, "package.json"), "utf8") ) as PackageJson; - if (pkgJson.main !== undefined) { + if (pkgJson.main) { pkgJson.main = stripDist(pkgJson.main); } - if (pkgJson.types !== undefined) { + if (pkgJson.types) { pkgJson.types = stripDist(pkgJson.types); } - if (pkgJson.exports !== undefined) { + if (pkgJson.exports) { pkgJson.exports = rewriteExports(pkgJson.exports); } delete pkgJson.files; + delete pkgJson["publishConfig"]; writeFileSync(join(distAbsDir, "package.json"), JSON.stringify(pkgJson, null, 2) + "\n"); } diff --git a/scripts/features/BuildPackages/BuildOrchestrator.ts b/scripts/features/BuildPackages/BuildOrchestrator.ts index d4ca0be..fc62ee0 100644 --- a/scripts/features/BuildPackages/BuildOrchestrator.ts +++ b/scripts/features/BuildPackages/BuildOrchestrator.ts @@ -4,7 +4,15 @@ import { Cleaner } from "./abstractions/Cleaner.ts"; import { Compiler } from "./abstractions/Compiler.ts"; import { ArtifactCopier } from "./abstractions/ArtifactCopier.ts"; import { PathAliasRewriter } from "./abstractions/PathAliasRewriter.ts"; -import { join } from "node:path"; +import { join, dirname, relative } from "node:path"; +import { + readdirSync, + mkdirSync, + copyFileSync, + existsSync, + readFileSync, + writeFileSync +} from "node:fs"; class BuildOrchestratorImpl implements BuildOrchestratorAbstraction.Interface { private readonly config: ProjectConfig.Interface; @@ -39,10 +47,44 @@ class BuildOrchestratorImpl implements BuildOrchestratorAbstraction.Interface { this.pathAliasRewriter.rewrite(distDir); + this.copyReadmes(rootDir); + this.ensureShebang(rootDir); + this.artifactCopier.copyPackageJson(rootDir, distDir); this.artifactCopier.copyReadme(rootDir, distDir); this.artifactCopier.copyLicense(rootDir, distDir); } + + private copyReadmes(rootDir: string): void { + const srcDir = join(rootDir, "src"); + const distDir = join(rootDir, "dist"); + this.walkForReadmes(srcDir, srcDir, distDir); + } + + private walkForReadmes(baseDir: string, dir: string, distDir: string): void { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const fullPath = join(dir, entry.name); + if (entry.isDirectory()) { + this.walkForReadmes(baseDir, fullPath, distDir); + } else if (entry.name === "README.md") { + const relPath = relative(baseDir, fullPath); + const destPath = join(distDir, relPath); + mkdirSync(dirname(destPath), { recursive: true }); + copyFileSync(fullPath, destPath); + } + } + } + + private ensureShebang(rootDir: string): void { + const cliPath = join(rootDir, "dist", "mcp", "cli.js"); + if (!existsSync(cliPath)) { + return; + } + const content = readFileSync(cliPath, "utf-8"); + if (!content.startsWith("#!")) { + writeFileSync(cliPath, "#!/usr/bin/env node\n" + content); + } + } } export const BuildOrchestrator = BuildOrchestratorAbstraction.createImplementation({ diff --git a/scripts/features/BuildPackages/index.ts b/scripts/features/BuildPackages/index.ts index 63743b0..8aa537e 100644 --- a/scripts/features/BuildPackages/index.ts +++ b/scripts/features/BuildPackages/index.ts @@ -13,7 +13,8 @@ export function run(rootDir: string): void { slices: [ "config/tsconfig.common.json", "config/tsconfig.node.json", - "config/tsconfig.browser.json" + "config/tsconfig.browser.json", + "config/tsconfig.mcp.json" ] }); container.register(CleanerImpl).inSingletonScope(); diff --git a/skills/adding-a-feature/SKILL.md b/skills/adding-a-feature/SKILL.md new file mode 100644 index 0000000..a9e9267 --- /dev/null +++ b/skills/adding-a-feature/SKILL.md @@ -0,0 +1,80 @@ +--- +name: adding-a-feature +description: Step-by-step guide for adding a new tool or service to @webiny/stdlib. +context: guides +--- + +# Adding a New Tool to @webiny/stdlib + +This mirrors the "Step-by-step: Adding a New Tool to an Existing Package" section in the repo's `AGENTS.md`, which is the authoritative source — check there if this drifts. + +First decide which slice the tool belongs in, based on its runtime dependencies: + +| Uses only JS built-ins / standard lib | → `@webiny/stdlib` root (`src/`) | +| Uses `node:*` APIs or Node-only npm packages | → `@webiny/stdlib/node` (`src/node/`) | +| Uses `window`, `document`, React, browser APIs | → `@webiny/stdlib/browser` (`src/browser/`) | + +`src/node/` and `src/browser/` must NOT import from each other. Both may import common code via `~/common/index.js`. + +Example below: adding `HttpTool` to `@webiny/stdlib/node`. Substitute the appropriate slice and path for your tool. + +## Steps + +1. **Create the abstraction** at `src/node/features/HttpTool/abstractions/HttpTool.ts`: + - Define `interface IHttpTool` with JSDoc on each method. + - Export `const HttpTool = createAbstraction("Core/HttpTool")`. + - Export `namespace HttpTool { export type Interface = IHttpTool }`. + +2. **Create the abstraction barrel** at `src/node/features/HttpTool/abstractions/index.ts`: + - `export { HttpTool } from "./HttpTool.js";` + +3. **Create the implementation** at `src/node/features/HttpTool/HttpTool.ts`: + - Rename the token import to avoid a name clash: `import { HttpTool as HttpToolAbstraction } from "./abstractions/HttpTool.js";` + - Import cross-slice dependencies via the common barrel: `import { Logger } from "~/common/index.js";` + - `class HttpToolImpl implements HttpToolAbstraction.Interface { ... }` + - `export const HttpTool = HttpToolAbstraction.createImplementation({ implementation: HttpToolImpl, dependencies: [...] })` + - The `dependencies` array order must match the constructor parameter order exactly. + +4. **Create the feature** at `src/node/features/HttpTool/feature.ts`: + - `export const HttpToolFeature = createFeature({ name: "Core/HttpToolFeature", register(container) { container.register(HttpTool).inSingletonScope(); } })` + +5. **Create the feature index** at `src/node/features/HttpTool/index.ts`: + - `export { HttpTool } from "./abstractions/index.js";` + - `export { HttpToolFeature } from "./feature.js";` + - Never re-export the implementation class or the `createImplementation` output. + +6. **Create the feature README** at `src/node/features/HttpTool/README.md`: + - One-paragraph description — what it does and when to reach for it. + - Interface section — the public methods with JSDoc (copy from the abstraction file). + - Usage section — two code snippets: DI container wiring, and a `createXxx()` factory function. + - Prepend YAML front-matter (`name`, `description`, `context`) so the README is discoverable by the MCP server — see the front-matter block at the top of any existing feature README for the exact shape. + - Update the repo root `README.md` table to add a row pointing to the new feature README. + +7. **Add to the slice barrel** `src/node/index.ts`: + - `export { HttpTool, HttpToolFeature } from "./features/HttpTool/index.js";` + +8. **Write tests** at `__tests__/node/HttpTool.test.ts`: + - Create a `makeContainer()` helper — see the `testing-patterns` skill. + - Cover the happy path and error paths. + - Node tests do not need a `// @vitest-environment` directive (only browser tests do). + +9. **Run the pre-commit chain** until fully clean: + + ```sh + yarn && yarn adio && yarn format:fix && yarn lint:fix && yarn typecheck && yarn build && yarn test:coverage + ``` + + All steps must pass with zero errors and zero warnings before staging and committing. If any step fails, fix the issue and run the full chain again from the start. + +## Conventions to keep in mind throughout + +- **No default exports.** Always use named exports. +- **JSDoc comments are preferred** on interface methods, abstraction types, and public class methods — they're read by both IDE tooling and agents. Don't write comments that just restate the method name. +- **Strict TypeScript** — all strict flags in `tsconfig.json` must pass. +- **`.js` extensions in package source imports** — under `src/`, `import { Foo } from "./Foo.js"` (not `.ts`); tsgo resolves `.js` to `.ts` at compile time. (Exception: `scripts/`, which runs directly under Node 24 and needs real `.ts` extensions.) +- **`node:` prefix for Node built-ins** — `import { readFileSync } from "node:fs"`, never `"fs"`. +- **Singletons via DI** — register utils as `.inSingletonScope()` unless there's a reason not to. +- **DI token names use `"Domain/ToolName"` format** — `"Core/"` prefix for common utils, `"Node/"` prefix for Node-specific ones. +- **Namespace pattern for interface types** — always export `namespace ToolName { export type Interface = IToolName }` alongside the token. + +See the `di-patterns` skill for the underlying abstraction/implementation/feature mechanics, and AGENTS.md's "Step-by-step: Adding a New Slice to @webiny/stdlib" section if you're adding an entirely new runtime environment rather than a tool within an existing one. diff --git a/skills/di-patterns/SKILL.md b/skills/di-patterns/SKILL.md new file mode 100644 index 0000000..734cb35 --- /dev/null +++ b/skills/di-patterns/SKILL.md @@ -0,0 +1,157 @@ +--- +name: di-patterns +description: How dependency injection works in @webiny/stdlib — abstractions, implementations, features, and container wiring. +context: guides +--- + +# DI Patterns in @webiny/stdlib + +Everything in this repo is built on constructor injection via `@webiny/di`. Tool code never uses `@webiny/di` directly — it always goes through the `createAbstraction` / `createFeature` wrappers exported from `@webiny/stdlib`. + +## Mental model + +Every service lives behind an **abstraction** — a typed token. Implementations are classes bound to that token. Consumers resolve the abstraction, never the implementation: + +```ts +import { createAbstraction } from "@webiny/stdlib"; + +interface IFileTool { + readFile(path: string): string | null; +} + +export const FileTool = createAbstraction("Core/FileTool"); + +export namespace FileTool { + export type Interface = IFileTool; +} +``` + +The `namespace FileTool { export type Interface }` pattern lets consumers write `FileTool.Interface` as the type and `FileTool` as the DI token — one import, both concepts. + +The token name (`"Core/FileTool"`) follows a `"Domain/ToolName"` format: `"Core/"` prefix for common utils, `"Node/"` prefix for Node-specific ones. It's used only for debug/error messages — uniqueness comes from an internal symbol, not the string. + +## createAbstraction + +```ts +const FileTool = createAbstraction("Core/FileTool"); +``` + +Creates the DI token. Always paired with the `namespace ... { export type Interface }` block described above. + +## createImplementation + +Attaches a concrete class to an abstraction token: + +```ts +import { Logger } from "~/common/index.js"; +import { FileTool as FileToolAbstraction } from "./abstractions/FileTool.js"; +import { DirectoryTool } from "../DirectoryTool/abstractions/DirectoryTool.js"; + +class FileToolImpl implements FileToolAbstraction.Interface { + public constructor( + private readonly logger: Logger.Interface, + private readonly directoryTool: DirectoryTool.Interface + ) {} + // ... +} + +export const FileTool = FileToolAbstraction.createImplementation({ + implementation: FileToolImpl, + dependencies: [Logger, DirectoryTool] // order MUST match constructor params exactly +}); +``` + +Note the local rename alias (`FileTool as FileToolAbstraction`) — it avoids a name collision, since the implementation file re-exports its own `const FileTool` (the `createImplementation` output). Consumers never import from this file directly; only `feature.ts` does. + +**The `dependencies` array is positional.** It must match the constructor parameter order exactly. TypeScript statically enforces this — a mismatched array is a compile error. + +### Optional dependencies + +Wrap the token in a tuple with `{ optional: true }`. The container passes `undefined` if nothing is registered for that token: + +```ts +export const PinoLogger = Logger.createImplementation({ + implementation: PinoLoggerImpl, + dependencies: [[PinoLoggerConfig, { optional: true }]] +}); +``` + +The constructor receives `PinoLoggerConfig.Interface | undefined` and must handle both cases. This is how `PinoLoggerFeature` and `ConsoleLoggerFeature` accept configuration without a required registration — callers who don't care about config just skip registering `PinoLoggerConfig`. + +## createFeature + +A "feature" bundles related registrations behind one name — the composition unit that bootstrap code calls instead of registering individual tokens. + +Most features take no parameters: + +```ts +import { createFeature } from "@webiny/stdlib"; +import { FileTool } from "./FileTool.js"; + +export const FileToolFeature = createFeature({ + name: "Core/FileToolFeature", + register(container) { + container.register(FileTool).inSingletonScope(); + } +}); +``` + +When a feature needs runtime configuration, use the typed parameter form: + +```ts +interface MyFeatureParams { + logLevel: "debug" | "info" | "warn" | "error"; +} + +export const MyFeature = createFeature({ + name: "Core/MyFeature", + register(container, params) { + // params is MyFeatureParams | undefined — use params! when required + container.registerInstance(Logger, makeLogger(params!.logLevel)); + } +}); +``` + +Call it as `MyFeature.register(container, { logLevel: "error" })`. The `params!` non-null assertion is required because `createFeature` types the parameter as `TRegister | undefined` to support both the parameterless and parameterized calling forms. + +Features compose — a larger feature can call other features from its own `register`, keeping the bootstrap/composition root tidy. + +## Container + +Used in application bootstrap and in tests to wire everything up: + +```ts +import { Container } from "@webiny/di"; + +const container = new Container(); +container.register(FileTool).inSingletonScope(); // class-based registration +container.registerInstance(Logger, myLoggerInstance); // pre-created instance +const tool = container.resolve(FileTool); // returns FileTool.Interface +``` + +- `register(ImplClass)` — registers an implementation class produced by `createImplementation`. Returns a builder; chain `.inSingletonScope()` for singleton lifetime. Default is transient (a new instance per `resolve`). +- `registerInstance(abstraction, value)` — registers a pre-built value. Always singleton in effect. +- `resolve(abstraction)` — returns a single instance. Throws if nothing is registered for the token. + +## Singleton scope + +**Always register utils as `.inSingletonScope()` unless there's a specific reason not to.** Without it, the container creates a new instance on every `resolve` call, which is almost never what you want for stateless services like loggers, caches, and filesystem tools. + +```ts +// Correct — one instance per container +container.register(FileTool).inSingletonScope(); + +// Wrong for a stateless tool — fresh instance every resolve +container.register(FileTool); +``` + +## The four-file layout + +Every tool/service in this repo follows the same shape: + +1. **`abstractions/ToolName.ts`** — interface, `createAbstraction` token, `namespace ToolName { export type Interface }`. +2. **`ToolName.ts`** — implementation class + `createImplementation`, using the local rename alias. +3. **`feature.ts`** — `createFeature` call that registers the implementation. +4. **`index.ts`** — re-exports the abstraction token and the feature. Never re-exports the implementation class or the `createImplementation` output — those stay internal to the feature folder. + +See the `adding-a-feature` skill for the full step-by-step when creating a new one. diff --git a/skills/testing-patterns/SKILL.md b/skills/testing-patterns/SKILL.md new file mode 100644 index 0000000..a6526e2 --- /dev/null +++ b/skills/testing-patterns/SKILL.md @@ -0,0 +1,113 @@ +--- +name: testing-patterns +description: Testing conventions for @webiny/stdlib — container setup, tmpdir patterns, browser environment directives. +context: guides +--- + +# Testing Patterns in @webiny/stdlib + +Tests live in `__tests__/` at the repo root (outside `src/`), organised into `__tests__/node/` and `__tests__/browser/`. A single `vitest.config.ts` at the repo root handles both test discovery and coverage — there is no workspace file. + +## Container setup — `makeContainer()` + +Every test file that tests a tool creates a `makeContainer()` helper. Silence logs during tests by registering a config instance with `logLevel: "error"` before registering the logger feature, then register the features under test. + +```ts +import { Container } from "@webiny/di"; +import { FileTool, FileToolFeature } from "../features/FileTool/index.js"; +import { DirectoryToolFeature } from "../features/DirectoryTool/index.js"; +import { PinoLoggerConfig, PinoLoggerFeature } from "@webiny/stdlib/node"; + +function makeContainer(): Container { + const container = new Container(); + container.registerInstance(PinoLoggerConfig, { + getConfig: () => ({ logLevel: "error" as const, transport: "json" as const }) + }); + PinoLoggerFeature.register(container); + DirectoryToolFeature.register(container); + FileToolFeature.register(container); + return container; +} +``` + +For stdlib common tests (testing with `ConsoleLogger` instead of `PinoLogger`), use `ConsoleLoggerConfig` and `ConsoleLoggerFeature` from `@webiny/stdlib` instead. + +Resolve instances in `beforeEach` so each test gets a fresh container — this avoids singleton state bleeding across tests: + +```ts +let tool: FileTool.Interface; + +beforeEach(() => { + tool = makeContainer().resolve(FileTool); +}); +``` + +**Rule**: never construct an implementation class directly in a test. Always resolve via the container, so the test exercises the same wiring as production. + +## tmpdir cleanup + +Use Node's `tmpdir()` for temporary directories in filesystem tests. Always clean up in `afterEach`: + +```ts +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { mkdirSync, rmSync } from "node:fs"; + +let tmpDir: string; + +beforeEach(() => { + tmpDir = join(tmpdir(), `wby-test-${Date.now()}`); + mkdirSync(tmpDir, { recursive: true }); +}); + +afterEach(() => { + rmSync(tmpDir, { recursive: true, force: true }); +}); +``` + +## Browser environment directive + +`vitest.config.ts` does NOT set a global `environment`. Browser tests opt into the happy-dom environment on a per-file basis using a directive at the very top of the test file: + +```ts +// __tests__/browser/LocalStorageCache.test.ts +// @vitest-environment happy-dom +import { ... } from "@webiny/stdlib/browser"; +``` + +Node tests need no environment directive — Vitest defaults to the Node environment. + +**happy-dom spy cleanup**: `vi.restoreAllMocks()` does not reliably clean up `vi.spyOn` calls on happy-dom objects (e.g. `localStorage.setItem`). Always call `spy.mockRestore()` explicitly in a `finally` block when spying on happy-dom storage. + +## Type-checking tests + +Test files must be type-correct. `yarn typecheck` covers `__tests__/` via each slice's check config (`tsconfig.check.common.json`, `tsconfig.check.node.json`, `tsconfig.check.browser.json`). Type errors in tests are caught before any test runs. + +## Coverage + +`yarn test:coverage` runs the full suite with v8 coverage in one invocation, using the coverage block in `vitest.config.ts`: + +```ts +coverage: { + provider: "v8", + include: ["src/**/*.ts"], + exclude: ["**/__tests__/**", "**/index.ts", "**/abstractions/**"] +} +``` + +Barrel files (`index.ts`) and abstraction files are excluded from coverage since they contain no executable logic worth measuring — only implementation and feature files are covered. + +## Running tests + +```sh +yarn test # all tests +yarn test:coverage # with v8 coverage +``` + +Both must pass as part of the pre-commit chain before committing: + +```sh +yarn && yarn adio && yarn format:fix && yarn lint:fix && yarn typecheck && yarn build && yarn test:coverage +``` + +See the `adding-a-feature` skill for where writing tests fits into the overall workflow when introducing a new tool. diff --git a/src/browser/features/BrowserEnv/README.md b/src/browser/features/BrowserEnv/README.md index 7ae8398..94bf509 100644 --- a/src/browser/features/BrowserEnv/README.md +++ b/src/browser/features/BrowserEnv/README.md @@ -1,3 +1,9 @@ +--- +name: browser-env +description: Browser implementation of the Env abstraction, backed by an injected Record of variables. +context: browser +--- + # BrowserEnv Browser implementation of the `Env` abstraction, backed by an injected `Record`. Pass variables at feature registration time; defaults to an empty object (all reads return `undefined` or the provided default). diff --git a/src/browser/features/LocalStorageCache/README.md b/src/browser/features/LocalStorageCache/README.md index 4206e08..467ac12 100644 --- a/src/browser/features/LocalStorageCache/README.md +++ b/src/browser/features/LocalStorageCache/README.md @@ -1,3 +1,9 @@ +--- +name: local-storage-cache +description: A Cache implementation backed by window.localStorage that returns Result instead of throwing. +context: browser +--- + # LocalStorageCache A `Cache` implementation backed by `window.localStorage`. All methods return `Result` — they never throw. When `localStorage` is unavailable (SSR, private-browsing restrictions), every operation returns a typed `LocalStorageUnavailableError` instead of crashing. diff --git a/src/common/features/Cache/README.md b/src/common/features/Cache/README.md index 33cd9fc..d663a4a 100644 --- a/src/common/features/Cache/README.md +++ b/src/common/features/Cache/README.md @@ -1,3 +1,9 @@ +--- +name: cache +description: Synchronous and async key-value caches with a Result-based API that never throw. +context: common +--- + # Cache / AsyncCache Synchronous and async key-value caches with a `Result`-based API — they never throw. All operations return `Result` (sync) or `ResultAsync` (async), so callers handle errors explicitly. diff --git a/src/common/features/Env/README.md b/src/common/features/Env/README.md index 8eb4ab2..1c2ae6b 100644 --- a/src/common/features/Env/README.md +++ b/src/common/features/Env/README.md @@ -1,3 +1,9 @@ +--- +name: env +description: Typed access to environment variables, with platform-specific implementations for Node and browser. +context: common +--- + # Env Typed access to environment variables. The `Env` abstraction lives in `@webiny/stdlib` (common) and has platform-specific implementations: `ProcessEnv` (Node, backed by `process.env`) and `BrowserEnv` (browser, backed by an injected variables object). diff --git a/src/common/features/Logger/README.md b/src/common/features/Logger/README.md index 4ecd1b5..ff2b537 100644 --- a/src/common/features/Logger/README.md +++ b/src/common/features/Logger/README.md @@ -1,3 +1,9 @@ +--- +name: logger +description: Shared DI logging token, with a console-based implementation supporting log levels, prefix, and timestamps. +context: common +--- + # Logger / ConsoleLogger `Logger` is the shared DI token for logging across all packages. `ConsoleLoggerFeature` registers a `console`-based implementation that supports log levels, optional prefix, and optional timestamps. Any package that injects `Logger` works with any registered implementation — swap `ConsoleLogger` for `PinoLogger` from `@webiny/stdlib/node` without changing call sites. diff --git a/src/common/utils/README.md b/src/common/utils/README.md index f1d55bc..966da9f 100644 --- a/src/common/utils/README.md +++ b/src/common/utils/README.md @@ -1,3 +1,9 @@ +--- +name: utils +description: Standalone utility functions exported from @webiny/stdlib — no DI container required. +context: common +--- + # Common Utils Standalone utility functions exported from `@webiny/stdlib`. No DI container required — import and call directly. diff --git a/src/common/utils/boolean/README.md b/src/common/utils/boolean/README.md index a2853dc..032bb13 100644 --- a/src/common/utils/boolean/README.md +++ b/src/common/utils/boolean/README.md @@ -1,3 +1,9 @@ +--- +name: boolean +description: Semantic boolean coercion for stringly-typed inputs such as environment variables and form values. +context: common +--- + # boolean Semantic boolean coercion with exact parity to the [`boolean`](https://www.npmjs.com/package/boolean) npm package. Converts environment variable strings, form values, and other stringly-typed inputs to `boolean` using a well-defined set of truthy tokens. diff --git a/src/common/utils/dotProp/README.md b/src/common/utils/dotProp/README.md index b242c25..5ee1316 100644 --- a/src/common/utils/dotProp/README.md +++ b/src/common/utils/dotProp/README.md @@ -1,3 +1,9 @@ +--- +name: dot-prop +description: Immutable and mutable get/set/delete operations on nested objects via dot-notation paths. +context: common +--- + # dotProp Immutable and mutable operations on nested objects via dot-notation paths. Wraps the [`dot-prop`](https://github.com/deoxxa/dot-prop) package with explicit immutable/mutable variants and `structuredClone`-based deep cloning. diff --git a/src/common/utils/generateId/README.md b/src/common/utils/generateId/README.md index 6e03588..4e88344 100644 --- a/src/common/utils/generateId/README.md +++ b/src/common/utils/generateId/README.md @@ -1,3 +1,9 @@ +--- +name: generate-id +description: Nanoid-based ID generators with configurable alphabets and sizes. +context: common +--- + # generateId Nanoid-based ID generators with configurable alphabets and sizes. Uses `nanoid` (v5) and `nanoid-dictionary` under the hood. All generators default to 21 characters and accept an optional `size` parameter. diff --git a/src/common/utils/mdbid/README.md b/src/common/utils/mdbid/README.md index 1c42df1..134ae8c 100644 --- a/src/common/utils/mdbid/README.md +++ b/src/common/utils/mdbid/README.md @@ -1,3 +1,9 @@ +--- +name: mdbid +description: Generates MongoDB-compatible ObjectId strings via bson-objectid. +context: common +--- + # mdbid Generates MongoDB-compatible ObjectId strings. Uses `bson-objectid` to produce 24-character, time-sortable, globally unique hex identifiers that match MongoDB's native ObjectId format. diff --git a/src/common/utils/uuid/README.md b/src/common/utils/uuid/README.md index 274d15f..ceced89 100644 --- a/src/common/utils/uuid/README.md +++ b/src/common/utils/uuid/README.md @@ -1,3 +1,9 @@ +--- +name: uuid +description: Generates RFC 4122 v4 UUIDs, with a native crypto fallback for insecure browser contexts. +context: common +--- + # uuid Generates RFC 4122 v4 UUIDs. Uses the native `crypto.randomUUID()` when available (Node.js, secure browser contexts) and falls back to a manual implementation using `crypto.getRandomValues()` for insecure (HTTP) browser contexts. No external dependencies. diff --git a/src/mcp/cli.ts b/src/mcp/cli.ts new file mode 100644 index 0000000..c056dbc --- /dev/null +++ b/src/mcp/cli.ts @@ -0,0 +1,79 @@ +#!/usr/bin/env node +import { existsSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { Container } from "@webiny/di"; +import { SkillDiscoveryConfig, McpServerFeature, McpServer } from "./features/Server/index.js"; +import { AgentConfiguratorFeature, AgentConfigurator } from "./features/Configure/index.js"; + +function parseFlags(argv: string[]): { skills: string[]; additionalSkills: string[] } { + const skills: string[] = []; + const additionalSkills: string[] = []; + + for (const arg of argv) { + if (arg.startsWith("--skills=")) { + skills.push(arg.slice("--skills=".length)); + } else if (arg.startsWith("--additional-skills=")) { + additionalSkills.push(arg.slice("--additional-skills=".length)); + } + } + + return { skills, additionalSkills }; +} + +function resolvePackageRoot(): string { + const thisFile = fileURLToPath(import.meta.url); + let dir = dirname(thisFile); + while (dir !== dirname(dir)) { + if (existsSync(join(dir, "package.json"))) { + return dir; + } + dir = dirname(dir); + } + return dirname(thisFile); +} + +function resolveDefaultScanPaths(packageRoot: string): string[] { + const paths: string[] = []; + + const skillsDir = join(packageRoot, "skills"); + if (existsSync(skillsDir)) { + paths.push(skillsDir); + } + + const srcDir = join(packageRoot, "src"); + const distDir = join(packageRoot, "dist"); + if (existsSync(srcDir)) { + paths.push(srcDir); + } else if (existsSync(distDir)) { + paths.push(distDir); + } + + return paths; +} + +const command = process.argv[2]; + +if (command === "serve") { + const flags = parseFlags(process.argv.slice(3)); + const packageRoot = resolvePackageRoot(); + + let scanPaths: string[]; + if (flags.skills.length > 0) { + scanPaths = [...flags.additionalSkills, ...flags.skills]; + } else { + scanPaths = [...flags.additionalSkills, ...resolveDefaultScanPaths(packageRoot)]; + } + + const container = new Container(); + container.registerInstance(SkillDiscoveryConfig, { scanPaths }); + McpServerFeature.register(container); + await container.resolve(McpServer).start(); +} else if (command === "configure") { + const container = new Container(); + AgentConfiguratorFeature.register(container); + await container.resolve(AgentConfigurator).configure(); +} else { + console.error("Usage: stdlib-mcp "); + process.exit(1); +} diff --git a/src/mcp/features/Configure/AgentConfigurator.ts b/src/mcp/features/Configure/AgentConfigurator.ts new file mode 100644 index 0000000..94fe0f4 --- /dev/null +++ b/src/mcp/features/Configure/AgentConfigurator.ts @@ -0,0 +1,53 @@ +import { createInterface } from "node:readline"; +import { AgentConfigurator as AgentConfiguratorAbstraction } from "./abstractions/AgentConfigurator.js"; +import type { AgentModule } from "./agents/types.js"; +import claudeAgent from "./agents/claude.js"; +import cursorAgent from "./agents/cursor.js"; +import clineAgent from "./agents/cline.js"; +import copilotAgent from "./agents/copilot.js"; +import windsurfAgent from "./agents/windsurf.js"; +import kiroAgent from "./agents/kiro.js"; +import opencodeAgent from "./agents/opencode.js"; + +const AGENTS: AgentModule[] = [ + claudeAgent, + clineAgent, + copilotAgent, + cursorAgent, + kiroAgent, + opencodeAgent, + windsurfAgent +].sort((a, b) => a.preset.displayName.localeCompare(b.preset.displayName)); + +class AgentConfiguratorImpl implements AgentConfiguratorAbstraction.Interface { + public async configure(): Promise { + console.log("\nSelect your AI agent:\n"); + for (let i = 0; i < AGENTS.length; i++) { + console.log(` ${i + 1}. ${AGENTS[i]!.preset.displayName}`); + } + console.log(); + + const rl = createInterface({ input: process.stdin, output: process.stdout }); + + const answer = await new Promise(resolve => { + rl.question("Enter number: ", resolve); + }); + rl.close(); + + const index = parseInt(answer, 10) - 1; + if (isNaN(index) || index < 0 || index >= AGENTS.length) { + console.error("Invalid selection."); + return; + } + + const agent = AGENTS[index]!; + console.log(`\nConfiguring ${agent.preset.displayName}...`); + await agent.init({ cwd: process.cwd() }); + console.log("Done."); + } +} + +export const AgentConfigurator = AgentConfiguratorAbstraction.createImplementation({ + implementation: AgentConfiguratorImpl, + dependencies: [] +}); diff --git a/src/mcp/features/Configure/README.md b/src/mcp/features/Configure/README.md new file mode 100644 index 0000000..cf22422 --- /dev/null +++ b/src/mcp/features/Configure/README.md @@ -0,0 +1,25 @@ +--- +name: mcp-configure +description: Interactive CLI that configures AI agents to use the stdlib MCP server. +context: guides +--- + +# Configure + +Interactive CLI that configures AI agents to use the stdlib MCP server. Supports Claude Code, Cursor, Cline, GitHub Copilot, Windsurf, Kiro, and OpenCode. + +## Interface + +- `configure(): Promise` — presents agent selection menu, writes MCP config and hint files for the chosen agent. + +## Usage + +```ts +import { Container } from "@webiny/di"; +import { AgentConfiguratorFeature, AgentConfigurator } from "@webiny/stdlib/mcp"; + +const container = new Container(); +AgentConfiguratorFeature.register(container); + +await container.resolve(AgentConfigurator).configure(); +``` diff --git a/src/mcp/features/Configure/abstractions/AgentConfigurator.ts b/src/mcp/features/Configure/abstractions/AgentConfigurator.ts new file mode 100644 index 0000000..0755d26 --- /dev/null +++ b/src/mcp/features/Configure/abstractions/AgentConfigurator.ts @@ -0,0 +1,11 @@ +import { createAbstraction } from "~/common/index.js"; + +export interface IAgentConfigurator { + configure(): Promise; +} + +export const AgentConfigurator = createAbstraction("Mcp/AgentConfigurator"); + +export namespace AgentConfigurator { + export type Interface = IAgentConfigurator; +} diff --git a/src/mcp/features/Configure/abstractions/index.ts b/src/mcp/features/Configure/abstractions/index.ts new file mode 100644 index 0000000..7299957 --- /dev/null +++ b/src/mcp/features/Configure/abstractions/index.ts @@ -0,0 +1 @@ +export { AgentConfigurator } from "./AgentConfigurator.js"; diff --git a/src/mcp/features/Configure/agents/claude.ts b/src/mcp/features/Configure/agents/claude.ts new file mode 100644 index 0000000..92213cd --- /dev/null +++ b/src/mcp/features/Configure/agents/claude.ts @@ -0,0 +1,18 @@ +import type { AgentModule } from "./types.js"; +import { writeMcpConfig, writeHintFile, stdlibHintBlock } from "./shared.js"; + +const module: AgentModule = { + preset: { + slug: "claude", + displayName: "Claude Code", + configFile: ".mcp.json", + configKey: "mcpServers", + hintFile: "CLAUDE.md" + }, + async init({ cwd }) { + writeMcpConfig({ cwd, configFile: ".mcp.json", configKey: "mcpServers" }); + writeHintFile({ cwd, hintFile: "CLAUDE.md", content: stdlibHintBlock() }); + } +}; + +export default module; diff --git a/src/mcp/features/Configure/agents/cline.ts b/src/mcp/features/Configure/agents/cline.ts new file mode 100644 index 0000000..c119343 --- /dev/null +++ b/src/mcp/features/Configure/agents/cline.ts @@ -0,0 +1,20 @@ +import type { AgentModule } from "./types.js"; +import { writeMcpConfig } from "./shared.js"; + +const module: AgentModule = { + preset: { + slug: "cline", + displayName: "Cline", + configFile: ".vscode/cline_mcp_settings.json", + configKey: "mcpServers" + }, + async init({ cwd }) { + writeMcpConfig({ + cwd, + configFile: ".vscode/cline_mcp_settings.json", + configKey: "mcpServers" + }); + } +}; + +export default module; diff --git a/src/mcp/features/Configure/agents/copilot.ts b/src/mcp/features/Configure/agents/copilot.ts new file mode 100644 index 0000000..9df0326 --- /dev/null +++ b/src/mcp/features/Configure/agents/copilot.ts @@ -0,0 +1,22 @@ +import type { AgentModule } from "./types.js"; +import { writeMcpConfig, writeHintFile, stdlibHintBlock } from "./shared.js"; + +const module: AgentModule = { + preset: { + slug: "copilot", + displayName: "GitHub Copilot", + configFile: ".vscode/mcp.json", + configKey: "servers", + hintFile: ".github/copilot-instructions.md" + }, + async init({ cwd }) { + writeMcpConfig({ cwd, configFile: ".vscode/mcp.json", configKey: "servers" }); + writeHintFile({ + cwd, + hintFile: ".github/copilot-instructions.md", + content: stdlibHintBlock() + }); + } +}; + +export default module; diff --git a/src/mcp/features/Configure/agents/cursor.ts b/src/mcp/features/Configure/agents/cursor.ts new file mode 100644 index 0000000..1db28a3 --- /dev/null +++ b/src/mcp/features/Configure/agents/cursor.ts @@ -0,0 +1,18 @@ +import type { AgentModule } from "./types.js"; +import { writeMcpConfig, writeHintFile, stdlibHintBlock } from "./shared.js"; + +const module: AgentModule = { + preset: { + slug: "cursor", + displayName: "Cursor", + configFile: ".cursor/mcp.json", + configKey: "mcpServers", + hintFile: ".cursor/rules/stdlib.mdc" + }, + async init({ cwd }) { + writeMcpConfig({ cwd, configFile: ".cursor/mcp.json", configKey: "mcpServers" }); + writeHintFile({ cwd, hintFile: ".cursor/rules/stdlib.mdc", content: stdlibHintBlock() }); + } +}; + +export default module; diff --git a/src/mcp/features/Configure/agents/kiro.ts b/src/mcp/features/Configure/agents/kiro.ts new file mode 100644 index 0000000..89917f5 --- /dev/null +++ b/src/mcp/features/Configure/agents/kiro.ts @@ -0,0 +1,22 @@ +import type { AgentModule } from "./types.js"; +import { writeMcpConfig, writeHintFile, stdlibHintBlock } from "./shared.js"; + +const module: AgentModule = { + preset: { + slug: "kiro", + displayName: "Kiro", + configFile: ".kiro/settings/mcp.json", + configKey: "mcpServers", + hintFile: "AGENTS.md" + }, + async init({ cwd }) { + writeMcpConfig({ + cwd, + configFile: ".kiro/settings/mcp.json", + configKey: "mcpServers" + }); + writeHintFile({ cwd, hintFile: "AGENTS.md", content: stdlibHintBlock() }); + } +}; + +export default module; diff --git a/src/mcp/features/Configure/agents/opencode.ts b/src/mcp/features/Configure/agents/opencode.ts new file mode 100644 index 0000000..4a5b29d --- /dev/null +++ b/src/mcp/features/Configure/agents/opencode.ts @@ -0,0 +1,40 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import type { AgentModule } from "./types.js"; +import { writeHintFile, stdlibHintBlock } from "./shared.js"; + +const module: AgentModule = { + preset: { + slug: "opencode", + displayName: "OpenCode", + configFile: "opencode.json", + configKey: "mcp", + hintFile: "AGENTS.md" + }, + async init({ cwd }) { + const configPath = join(cwd, "opencode.json"); + mkdirSync(dirname(configPath), { recursive: true }); + + let existing: Record = {}; + if (existsSync(configPath)) { + existing = JSON.parse(readFileSync(configPath, "utf-8")) as Record; + } + + const section = (existing["mcp"] ?? {}) as Record; + if (section["stdlib"]) { + return; + } + + section["stdlib"] = { + type: "stdio", + command: ["npx", "stdlib-mcp", "serve"], + enabled: true + }; + existing["mcp"] = section; + writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n"); + + writeHintFile({ cwd, hintFile: "AGENTS.md", content: stdlibHintBlock() }); + } +}; + +export default module; diff --git a/src/mcp/features/Configure/agents/shared.ts b/src/mcp/features/Configure/agents/shared.ts new file mode 100644 index 0000000..fbe5528 --- /dev/null +++ b/src/mcp/features/Configure/agents/shared.ts @@ -0,0 +1,62 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; + +interface WriteMcpConfigParams { + cwd: string; + configFile: string; + configKey: string; +} + +interface WriteHintFileParams { + cwd: string; + hintFile: string; + content: string; +} + +export function writeMcpConfig({ cwd, configFile, configKey }: WriteMcpConfigParams): void { + const configPath = join(cwd, configFile); + mkdirSync(dirname(configPath), { recursive: true }); + + let existing: Record = {}; + if (existsSync(configPath)) { + existing = JSON.parse(readFileSync(configPath, "utf-8")) as Record; + } + + const section = (existing[configKey] ?? {}) as Record; + if (section["stdlib"]) { + return; + } + + section["stdlib"] = { + command: "npx", + args: ["stdlib-mcp", "serve"] + }; + existing[configKey] = section; + + writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n"); +} + +export function writeHintFile({ cwd, hintFile, content }: WriteHintFileParams): void { + const hintPath = join(cwd, hintFile); + mkdirSync(dirname(hintPath), { recursive: true }); + + if (existsSync(hintPath)) { + const existing = readFileSync(hintPath, "utf-8"); + if (existing.includes("list_stdlib_skills")) { + return; + } + const separator = existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n"; + writeFileSync(hintPath, existing + separator + content + "\n"); + } else { + writeFileSync(hintPath, content + "\n"); + } +} + +export function stdlibHintBlock(): string { + return `## @webiny/stdlib MCP + +This project uses \`@webiny/stdlib\`. An MCP server is available with tools for discovering stdlib features: + +- \`list_stdlib_skills\` — returns a catalog of all available skills with names and descriptions. Call this first when working with @webiny/stdlib. +- \`get_stdlib_skill\` — loads full documentation for a specific skill by name.`; +} diff --git a/src/mcp/features/Configure/agents/types.ts b/src/mcp/features/Configure/agents/types.ts new file mode 100644 index 0000000..ceaee6f --- /dev/null +++ b/src/mcp/features/Configure/agents/types.ts @@ -0,0 +1,12 @@ +export interface AgentPreset { + slug: string; + displayName: string; + configFile: string; + configKey: string; + hintFile?: string; +} + +export interface AgentModule { + preset: AgentPreset; + init: (params: { cwd: string }) => Promise; +} diff --git a/src/mcp/features/Configure/agents/windsurf.ts b/src/mcp/features/Configure/agents/windsurf.ts new file mode 100644 index 0000000..1d81ea5 --- /dev/null +++ b/src/mcp/features/Configure/agents/windsurf.ts @@ -0,0 +1,22 @@ +import type { AgentModule } from "./types.js"; +import { writeMcpConfig, writeHintFile, stdlibHintBlock } from "./shared.js"; + +const module: AgentModule = { + preset: { + slug: "windsurf", + displayName: "Windsurf", + configFile: ".windsurf/mcp.json", + configKey: "mcpServers", + hintFile: ".windsurf/rules/stdlib.md" + }, + async init({ cwd }) { + writeMcpConfig({ cwd, configFile: ".windsurf/mcp.json", configKey: "mcpServers" }); + writeHintFile({ + cwd, + hintFile: ".windsurf/rules/stdlib.md", + content: stdlibHintBlock() + }); + } +}; + +export default module; diff --git a/src/mcp/features/Configure/feature.ts b/src/mcp/features/Configure/feature.ts new file mode 100644 index 0000000..6436efd --- /dev/null +++ b/src/mcp/features/Configure/feature.ts @@ -0,0 +1,9 @@ +import { createFeature } from "~/common/index.js"; +import { AgentConfigurator } from "./AgentConfigurator.js"; + +export const AgentConfiguratorFeature = createFeature({ + name: "Mcp/AgentConfiguratorFeature", + register(container) { + container.register(AgentConfigurator).inSingletonScope(); + } +}); diff --git a/src/mcp/features/Configure/index.ts b/src/mcp/features/Configure/index.ts new file mode 100644 index 0000000..a383b04 --- /dev/null +++ b/src/mcp/features/Configure/index.ts @@ -0,0 +1,4 @@ +export { AgentConfigurator } from "./abstractions/index.js"; +export { AgentConfiguratorFeature } from "./feature.js"; +export type { AgentPreset, AgentModule } from "./agents/types.js"; +export { writeMcpConfig, writeHintFile, stdlibHintBlock } from "./agents/shared.js"; diff --git a/src/mcp/features/Server/McpServer.ts b/src/mcp/features/Server/McpServer.ts new file mode 100644 index 0000000..d481ef7 --- /dev/null +++ b/src/mcp/features/Server/McpServer.ts @@ -0,0 +1,124 @@ +import { McpServer as SdkMcpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; +import { z } from "zod"; +import { McpServer as McpServerAbstraction } from "./abstractions/McpServer.js"; +import { SkillDiscovery, type Skill } from "./abstractions/SkillDiscovery.js"; + +const CONTEXT_HEADINGS: Record = { + common: "Platform-agnostic utilities", + node: "Node.js-specific tools", + browser: "Browser-specific tools", + guides: "Cross-cutting guides and patterns" +}; + +const CONTEXT_ORDER = ["common", "node", "browser", "guides"]; + +function buildCatalog(skills: Skill[]): string { + if (skills.length === 0) { + return "No skills found."; + } + + const groups = new Map(); + for (const skill of skills) { + const existing = groups.get(skill.context); + if (existing) { + existing.push(skill); + } else { + groups.set(skill.context, [skill]); + } + } + + for (const [, group] of groups) { + group.sort((a, b) => a.name.localeCompare(b.name)); + } + + const sections: string[] = []; + const orderedContexts = [ + ...CONTEXT_ORDER.filter(c => groups.has(c)), + ...[...groups.keys()].filter(c => !CONTEXT_ORDER.includes(c)).sort() + ]; + + for (const ctx of orderedContexts) { + const heading = CONTEXT_HEADINGS[ctx] ?? ctx; + const group = groups.get(ctx)!; + const rows = group.map(s => `| ${s.name} | ${s.description} |`).join("\n"); + sections.push(`## ${heading}\n\n| Skill | Description |\n|-------|-------------|\n${rows}`); + } + + return sections.join("\n\n"); +} + +class McpServerImpl implements McpServerAbstraction.Interface { + public constructor(private readonly skillDiscovery: SkillDiscovery.Interface) {} + + public async start(): Promise { + const transport = new StdioServerTransport(); + await this.startWithTransport(transport); + } + + public async startWithTransport(transport: Transport): Promise { + const skills = this.skillDiscovery.discover(); + const skillMap = new Map(); + for (const skill of skills) { + skillMap.set(skill.name, skill); + } + + const server = new SdkMcpServer({ name: "stdlib", version: "1.0.0" }); + + server.registerTool( + "list_stdlib_skills", + { + title: "List stdlib skills", + description: + "Returns a catalog of all available @webiny/stdlib skills. Call this first when working with @webiny/stdlib, then call get_stdlib_skill to load a specific skill.", + annotations: { readOnlyHint: true } + }, + () => ({ + content: [{ type: "text" as const, text: buildCatalog(skills) }] + }) + ); + + server.registerTool( + "get_stdlib_skill", + { + title: "Get stdlib skill", + description: + "Loads full documentation for a specific @webiny/stdlib skill. Call list_stdlib_skills first to see available names.", + inputSchema: { topic: z.string().describe("Skill name from list_stdlib_skills") }, + annotations: { readOnlyHint: true } + }, + ({ topic }) => { + const skill = skillMap.get(topic); + if (!skill) { + const available = [...skillMap.keys()] + .sort() + .map(n => { + const s = skillMap.get(n)!; + return `- ${n} (${s.context})`; + }) + .join("\n"); + return { + isError: true, + content: [ + { + type: "text" as const, + text: `Skill "${topic}" not found. Available skills:\n${available}` + } + ] + }; + } + return { + content: [{ type: "text" as const, text: skill.body }] + }; + } + ); + + await server.connect(transport); + } +} + +export const McpServer = McpServerAbstraction.createImplementation({ + implementation: McpServerImpl, + dependencies: [SkillDiscovery] +}); diff --git a/src/mcp/features/Server/README.md b/src/mcp/features/Server/README.md new file mode 100644 index 0000000..1660da9 --- /dev/null +++ b/src/mcp/features/Server/README.md @@ -0,0 +1,35 @@ +--- +name: mcp-server +description: Exposes @webiny/stdlib feature documentation as MCP tools for AI agent discovery. +context: guides +--- + +# MCP Server + +Exposes `@webiny/stdlib` feature documentation as MCP tools for AI agent discovery. Two tools: `list_stdlib_skills` returns a grouped catalog; `get_stdlib_skill` loads full documentation for a specific skill. + +## Interface + +### SkillDiscovery + +- `discover(): Skill[]` — scans configured directories for README.md and SKILL.md files with YAML front-matter (via `@11ty/gray-matter`). Returns parsed skills. + +### McpServer + +- `start(): Promise` — starts the MCP server on stdio transport. Blocks until stdin closes. +- `startWithTransport(transport): Promise` — starts with a custom transport (used for testing). + +## Usage + +```ts +import { Container } from "@webiny/di"; +import { SkillDiscoveryConfig, McpServerFeature, McpServer } from "@webiny/stdlib/mcp"; + +const container = new Container(); +container.registerInstance(SkillDiscoveryConfig, { + scanPaths: ["./skills", "./src"] +}); +McpServerFeature.register(container); + +await container.resolve(McpServer).start(); +``` diff --git a/src/mcp/features/Server/SkillDiscovery.ts b/src/mcp/features/Server/SkillDiscovery.ts new file mode 100644 index 0000000..ad858b0 --- /dev/null +++ b/src/mcp/features/Server/SkillDiscovery.ts @@ -0,0 +1,101 @@ +import { readdirSync, readFileSync, existsSync } from "node:fs"; +import { join } from "node:path"; +import matter from "@11ty/gray-matter"; +import { + SkillDiscovery as SkillDiscoveryAbstraction, + type Skill +} from "./abstractions/SkillDiscovery.js"; +import { SkillDiscoveryConfig } from "./abstractions/SkillDiscoveryConfig.js"; + +class SkillDiscoveryImpl implements SkillDiscoveryAbstraction.Interface { + public constructor(private readonly config: SkillDiscoveryConfig.Interface) {} + + public discover(): Skill[] { + const seen = new Set(); + const skills: Skill[] = []; + + for (const scanPath of this.config.scanPaths) { + if (!existsSync(scanPath)) { + continue; + } + this.walk(scanPath, seen, skills); + } + + return skills; + } + + private walk(dir: string, seen: Set, skills: Skill[]): void { + let entries; + try { + entries = readdirSync(dir, { withFileTypes: true }); + } catch { + return; + } + + for (const entry of entries) { + const fullPath = join(dir, entry.name); + if (entry.isDirectory()) { + this.walk(fullPath, seen, skills); + } else if (entry.name === "README.md" || entry.name === "SKILL.md") { + this.tryParseSkill(fullPath, seen, skills); + } + } + } + + private tryParseSkill(filePath: string, seen: Set, skills: Skill[]): void { + let raw: string; + try { + raw = readFileSync(filePath, "utf-8"); + } catch { + console.warn(`Skipping unreadable file: ${filePath}`); + return; + } + + if (!matter.test(raw)) { + return; + } + + let parsed; + try { + parsed = matter(raw); + } catch { + console.warn(`Skipping ${filePath}: invalid YAML front-matter`); + return; + } + + const { name, description, context } = parsed.data as Record; + + if (typeof name !== "string" || name === "") { + console.warn(`Skipping ${filePath}: missing or empty "name" field`); + return; + } + + if (typeof description !== "string" || description === "") { + console.warn(`Skipping ${filePath}: missing or empty "description" field`); + return; + } + + if (context !== undefined && typeof context !== "string") { + console.warn(`Skipping ${filePath}: "context" must be a string`); + return; + } + + // First-match-wins: earlier scanPaths take priority on name collision. + if (seen.has(name)) { + return; + } + + seen.add(name); + skills.push({ + name, + description, + context: typeof context === "string" ? context : "common", + body: parsed.content.trim() + }); + } +} + +export const SkillDiscovery = SkillDiscoveryAbstraction.createImplementation({ + implementation: SkillDiscoveryImpl, + dependencies: [SkillDiscoveryConfig] +}); diff --git a/src/mcp/features/Server/abstractions/McpServer.ts b/src/mcp/features/Server/abstractions/McpServer.ts new file mode 100644 index 0000000..e77f688 --- /dev/null +++ b/src/mcp/features/Server/abstractions/McpServer.ts @@ -0,0 +1,13 @@ +import { createAbstraction } from "~/common/index.js"; +import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; + +export interface IMcpServer { + start(): Promise; + startWithTransport(transport: Transport): Promise; +} + +export const McpServer = createAbstraction("Mcp/McpServer"); + +export namespace McpServer { + export type Interface = IMcpServer; +} diff --git a/src/mcp/features/Server/abstractions/SkillDiscovery.ts b/src/mcp/features/Server/abstractions/SkillDiscovery.ts new file mode 100644 index 0000000..d6942fb --- /dev/null +++ b/src/mcp/features/Server/abstractions/SkillDiscovery.ts @@ -0,0 +1,29 @@ +import { createAbstraction } from "~/common/index.js"; + +/** + * A discovered skill, parsed from a README.md/SKILL.md file's + * YAML front-matter and Markdown body. + */ +export interface Skill { + name: string; + description: string; + context: string; + body: string; +} + +export interface ISkillDiscovery { + /** + * Recursively scans the configured `scanPaths` for README.md and + * SKILL.md files with valid YAML front-matter, returning one + * Skill per file. Files that are missing front-matter, or missing + * required `name`/`description` fields, are skipped with a warning. + * On a `name` collision across scan paths, the first match wins. + */ + discover(): Skill[]; +} + +export const SkillDiscovery = createAbstraction("Mcp/SkillDiscovery"); + +export namespace SkillDiscovery { + export type Interface = ISkillDiscovery; +} diff --git a/src/mcp/features/Server/abstractions/SkillDiscoveryConfig.ts b/src/mcp/features/Server/abstractions/SkillDiscoveryConfig.ts new file mode 100644 index 0000000..4bc287e --- /dev/null +++ b/src/mcp/features/Server/abstractions/SkillDiscoveryConfig.ts @@ -0,0 +1,17 @@ +import { createAbstraction } from "~/common/index.js"; + +/** + * Configuration for skill discovery — directories to scan for + * README.md / SKILL.md files containing YAML front-matter. + */ +export interface ISkillDiscoveryConfig { + scanPaths: string[]; +} + +export const SkillDiscoveryConfig = createAbstraction( + "Mcp/SkillDiscoveryConfig" +); + +export namespace SkillDiscoveryConfig { + export type Interface = ISkillDiscoveryConfig; +} diff --git a/src/mcp/features/Server/abstractions/index.ts b/src/mcp/features/Server/abstractions/index.ts new file mode 100644 index 0000000..6259e13 --- /dev/null +++ b/src/mcp/features/Server/abstractions/index.ts @@ -0,0 +1,3 @@ +export { SkillDiscovery, type Skill } from "./SkillDiscovery.js"; +export { SkillDiscoveryConfig } from "./SkillDiscoveryConfig.js"; +export { McpServer } from "./McpServer.js"; diff --git a/src/mcp/features/Server/feature.ts b/src/mcp/features/Server/feature.ts new file mode 100644 index 0000000..2fd186c --- /dev/null +++ b/src/mcp/features/Server/feature.ts @@ -0,0 +1,11 @@ +import { createFeature } from "~/common/index.js"; +import { SkillDiscovery } from "./SkillDiscovery.js"; +import { McpServer } from "./McpServer.js"; + +export const McpServerFeature = createFeature({ + name: "Mcp/McpServerFeature", + register(container) { + container.register(SkillDiscovery).inSingletonScope(); + container.register(McpServer).inSingletonScope(); + } +}); diff --git a/src/mcp/features/Server/index.ts b/src/mcp/features/Server/index.ts new file mode 100644 index 0000000..f22632f --- /dev/null +++ b/src/mcp/features/Server/index.ts @@ -0,0 +1,7 @@ +export { + SkillDiscovery, + SkillDiscoveryConfig, + McpServer, + type Skill +} from "./abstractions/index.js"; +export { McpServerFeature } from "./feature.js"; diff --git a/src/mcp/index.ts b/src/mcp/index.ts new file mode 100644 index 0000000..9d55bf5 --- /dev/null +++ b/src/mcp/index.ts @@ -0,0 +1,17 @@ +export { + SkillDiscovery, + SkillDiscoveryConfig, + McpServer, + McpServerFeature, + type Skill +} from "./features/Server/index.js"; + +export { + AgentConfigurator, + AgentConfiguratorFeature, + type AgentPreset, + type AgentModule, + writeMcpConfig, + writeHintFile, + stdlibHintBlock +} from "./features/Configure/index.js"; diff --git a/src/node/features/DirectoryTool/README.md b/src/node/features/DirectoryTool/README.md index 1dfa7cc..473c0a0 100644 --- a/src/node/features/DirectoryTool/README.md +++ b/src/node/features/DirectoryTool/README.md @@ -1,3 +1,9 @@ +--- +name: directory-tool +description: Creates, reads, copies, removes, and globs directories on the local filesystem. All paths must be absolute. +context: node +--- + # DirectoryTool Creates, reads, copies, removes, and globs directories on the local filesystem. All paths must be absolute. `create` is idempotent — it calls `mkdirSync` with `recursive: true` and is safe to call on an existing path. Methods without `OrThrow` log a warning and return `null` / `void` on failure; `OrThrow` variants throw. diff --git a/src/node/features/FileTool/README.md b/src/node/features/FileTool/README.md index f4d0ffd..34a48c7 100644 --- a/src/node/features/FileTool/README.md +++ b/src/node/features/FileTool/README.md @@ -1,3 +1,9 @@ +--- +name: file-tool +description: Read, write, copy, remove files. All paths must be absolute. +context: node +--- + # FileTool Reads, writes, copies, and removes files on the local filesystem. All paths must be absolute. Write operations automatically create missing parent directories. Methods without `OrThrow` log a warning and return `null` / `void` on failure; `OrThrow` variants throw. diff --git a/src/node/features/HashFolderTool/README.md b/src/node/features/HashFolderTool/README.md index 1cf891c..85890b1 100644 --- a/src/node/features/HashFolderTool/README.md +++ b/src/node/features/HashFolderTool/README.md @@ -1,3 +1,9 @@ +--- +name: hash-folder-tool +description: Computes a deterministic SHA-256 hash of a folder's contents to detect changes. +context: node +--- + # HashFolderTool Computes a deterministic SHA-256 hash of a folder's contents. Walks the directory tree recursively, hashes each file individually, sorts entries by relative path for deterministic ordering, then produces a single combined hex digest. Use it to detect whether a folder's contents have changed — for example, to skip redundant builds when source files haven't been modified. diff --git a/src/node/features/JsonFileTool/README.md b/src/node/features/JsonFileTool/README.md index b886641..5af5b0a 100644 --- a/src/node/features/JsonFileTool/README.md +++ b/src/node/features/JsonFileTool/README.md @@ -1,3 +1,9 @@ +--- +name: json-file-tool +description: Reads and writes JSON files on the local filesystem, with optional schema validation. +context: node +--- + # JsonFileTool Reads and writes JSON files on the local filesystem. Optionally validates the parsed value through a schema (any object with a `.parse(unknown): T` method — compatible with Zod, Valibot, and similar). Methods without `OrThrow` return `null` on failure; `OrThrow` variants throw. diff --git a/src/node/features/NdJsonReaderTool/README.md b/src/node/features/NdJsonReaderTool/README.md index a6f5665..58aa9bc 100644 --- a/src/node/features/NdJsonReaderTool/README.md +++ b/src/node/features/NdJsonReaderTool/README.md @@ -1,3 +1,9 @@ +--- +name: ndjson-reader-tool +description: Parses NDJSON data from a file, a Readable stream, or an in-memory iterable, with checkpoint resume support. +context: node +--- + # NdJsonReaderTool Parses [NDJSON](https://ndjson.org/) (newline-delimited JSON) data from a file path, a Node.js `Readable` stream, or an in-memory iterable of lines. Handles malformed input where a single JSON value is split across multiple lines by attempting newline-join and concatenation strategies before discarding and moving on. diff --git a/src/node/features/PackageJsonFileTool/README.md b/src/node/features/PackageJsonFileTool/README.md index 56fe8d6..c794197 100644 --- a/src/node/features/PackageJsonFileTool/README.md +++ b/src/node/features/PackageJsonFileTool/README.md @@ -1,3 +1,9 @@ +--- +name: package-json-file-tool +description: Reads, validates, and writes package.json files with typed mutation helpers for dependency sections. +context: node +--- + # PackageJsonFileTool Reads, validates, and writes `package.json` files. `read` and `readOrThrow` return a `PackageJsonFile` value object that carries the file path and parsed data and exposes mutation helpers for all dependency sections. Root-level well-known fields are validated at runtime with Zod; unknown fields pass through unchanged. The parsed data is typed as `PackageJson` from [type-fest](https://github.com/sindresorhus/type-fest). diff --git a/src/node/features/PathTool/README.md b/src/node/features/PathTool/README.md index dc5ed8d..b956125 100644 --- a/src/node/features/PathTool/README.md +++ b/src/node/features/PathTool/README.md @@ -1,3 +1,9 @@ +--- +name: path-tool +description: Injectable wrapper around node:path, plus package-relative file resolution via resolvePackageFile. +context: node +--- + # PathTool Wraps the four most-used `node:path` methods — `join`, `resolve`, `dirname`, and `basename` — behind the standard DI abstraction/implementation pattern. Also provides `resolvePackageFile` to turn a package-relative specifier into an absolute filesystem path via Node's module resolver. diff --git a/src/node/features/PinoLogger/README.md b/src/node/features/PinoLogger/README.md index e7542cc..fa05bfe 100644 --- a/src/node/features/PinoLogger/README.md +++ b/src/node/features/PinoLogger/README.md @@ -1,3 +1,9 @@ +--- +name: pino-logger +description: A pino-based implementation of the Logger abstraction from @webiny/stdlib. +context: node +--- + # PinoLogger A [pino](https://github.com/pinojs/pino)-based implementation of the `Logger` abstraction from `@webiny/stdlib`. Registers under the shared `Logger` DI token, so any tool that depends on `Logger` will receive a pino instance when `PinoLoggerFeature` is registered. diff --git a/src/node/features/ProcessEnv/README.md b/src/node/features/ProcessEnv/README.md index aaf6b83..17d06c6 100644 --- a/src/node/features/ProcessEnv/README.md +++ b/src/node/features/ProcessEnv/README.md @@ -1,3 +1,9 @@ +--- +name: process-env +description: Node.js implementation of the Env abstraction, backed by process.env by default. +context: node +--- + # ProcessEnv Node.js implementation of the `Env` abstraction, backed by `process.env` by default. Accepts an optional `variables` record to override the source. Provides typed access to environment variables with `getString`, `getNumber`, and `getBoolean` families. diff --git a/src/node/features/ReadStreamFactory/README.md b/src/node/features/ReadStreamFactory/README.md index 5ff70ab..f0db080 100644 --- a/src/node/features/ReadStreamFactory/README.md +++ b/src/node/features/ReadStreamFactory/README.md @@ -1,3 +1,9 @@ +--- +name: read-stream-factory +description: Creates node:fs read streams with explicit cleanup via destroy(). +context: node +--- + # ReadStreamFactory Creates `node:fs` read streams with explicit cleanup. Call `destroy()` to release the underlying file handle when done — including early `break` from an async generator loop or thrown errors (use try/finally). diff --git a/src/node/features/WorkspaceTool/README.md b/src/node/features/WorkspaceTool/README.md index 6bce846..76dcb0d 100644 --- a/src/node/features/WorkspaceTool/README.md +++ b/src/node/features/WorkspaceTool/README.md @@ -1,3 +1,9 @@ +--- +name: workspace-tool +description: Discovers workspaces defined in the nearest root package.json. +context: node +--- + # WorkspaceTool Discovers workspaces defined in the nearest root `package.json`. Walks up from a given directory to find a `package.json` with a `workspaces` field, then resolves the glob patterns to return workspace metadata. Supports both flat array (`workspaces: ["packages/*"]`) and Yarn's object format (`workspaces: { packages: ["packages/*"] }`). Drop-in replacement for the `get-yarn-workspaces` package. diff --git a/testing/vitest.config.ts b/testing/vitest.config.ts index a98cd3e..54358ae 100644 --- a/testing/vitest.config.ts +++ b/testing/vitest.config.ts @@ -16,7 +16,14 @@ export default defineConfig({ coverage: { provider: "v8", include: ["src/**/*.ts"], - exclude: ["**/__tests__/**", "**/index.ts", "**/abstractions/**", "**/feature.ts"], + exclude: [ + "**/__tests__/**", + "**/index.ts", + "**/abstractions/**", + "**/feature.ts", + "**/cli.ts", + "**/AgentConfigurator.ts" + ], thresholds: { statements: 96, branches: 93, diff --git a/tsconfig.json b/tsconfig.json index 5030c47..bbc8835 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,6 +32,7 @@ "references": [ { "path": "./config/tsconfig.common.json" }, { "path": "./config/tsconfig.node.json" }, - { "path": "./config/tsconfig.browser.json" } + { "path": "./config/tsconfig.browser.json" }, + { "path": "./config/tsconfig.mcp.json" } ] } diff --git a/yarn.lock b/yarn.lock index 1e91cdd..332f430 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,6 +5,16 @@ __metadata: version: 10 cacheKey: 10c0 +"@11ty/gray-matter@npm:^2.1.0": + version: 2.1.0 + resolution: "@11ty/gray-matter@npm:2.1.0" + dependencies: + js-yaml: "npm:^4.2.0" + section-matter: "npm:^1.0.0" + checksum: 10c0/9381fa8af2399ca1280008c04c1926899a168e105bd17e894cb4a6e70ce35fa86ea4ce6ab70de3cbe76dbd4554bdb87bf62e57ce984e88cec43d842fd6807a9c + languageName: node + linkType: hard + "@babel/code-frame@npm:^7.0.0": version: 7.29.7 resolution: "@babel/code-frame@npm:7.29.7" @@ -354,6 +364,15 @@ __metadata: languageName: node linkType: hard +"@hono/node-server@npm:^1.19.9": + version: 1.19.15 + resolution: "@hono/node-server@npm:1.19.15" + peerDependencies: + hono: ^4 + checksum: 10c0/677be66df4d419fa062b1cc31e03d5be03ed9d2bfcabefaeea302c9f8c7145b48daab19da46729161c341fbfc9fedbaac8dac2224bdca73e0a6e0f22d7016740 + languageName: node + linkType: hard + "@inquirer/external-editor@npm:^1.0.2": version: 1.0.3 resolution: "@inquirer/external-editor@npm:1.0.3" @@ -428,6 +447,39 @@ __metadata: languageName: node linkType: hard +"@modelcontextprotocol/sdk@npm:^1.29.0": + version: 1.29.0 + resolution: "@modelcontextprotocol/sdk@npm:1.29.0" + dependencies: + "@hono/node-server": "npm:^1.19.9" + ajv: "npm:^8.17.1" + ajv-formats: "npm:^3.0.1" + content-type: "npm:^1.0.5" + cors: "npm:^2.8.5" + cross-spawn: "npm:^7.0.5" + eventsource: "npm:^3.0.2" + eventsource-parser: "npm:^3.0.0" + express: "npm:^5.2.1" + express-rate-limit: "npm:^8.2.1" + hono: "npm:^4.11.4" + jose: "npm:^6.1.3" + json-schema-typed: "npm:^8.0.2" + pkce-challenge: "npm:^5.0.0" + raw-body: "npm:^3.0.0" + zod: "npm:^3.25 || ^4.0" + zod-to-json-schema: "npm:^3.25.1" + peerDependencies: + "@cfworker/json-schema": ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + "@cfworker/json-schema": + optional: true + zod: + optional: false + checksum: 10c0/7c4bc339205b1652330cd4e6b121cc859079655f2b9c0506bbb15563ba0d07924bda3d949705530532db7f4d2cb86d633dc8f92bc32803d97c7bece2ac63e29f + languageName: node + linkType: hard + "@napi-rs/wasm-runtime@npm:^1.1.4, @napi-rs/wasm-runtime@npm:^1.1.6": version: 1.1.6 resolution: "@napi-rs/wasm-runtime@npm:1.1.6" @@ -625,268 +677,268 @@ __metadata: languageName: node linkType: hard -"@oxfmt/binding-android-arm-eabi@npm:0.59.0": - version: 0.59.0 - resolution: "@oxfmt/binding-android-arm-eabi@npm:0.59.0" +"@oxfmt/binding-android-arm-eabi@npm:0.60.0": + version: 0.60.0 + resolution: "@oxfmt/binding-android-arm-eabi@npm:0.60.0" conditions: os=android & cpu=arm languageName: node linkType: hard -"@oxfmt/binding-android-arm64@npm:0.59.0": - version: 0.59.0 - resolution: "@oxfmt/binding-android-arm64@npm:0.59.0" +"@oxfmt/binding-android-arm64@npm:0.60.0": + version: 0.60.0 + resolution: "@oxfmt/binding-android-arm64@npm:0.60.0" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@oxfmt/binding-darwin-arm64@npm:0.59.0": - version: 0.59.0 - resolution: "@oxfmt/binding-darwin-arm64@npm:0.59.0" +"@oxfmt/binding-darwin-arm64@npm:0.60.0": + version: 0.60.0 + resolution: "@oxfmt/binding-darwin-arm64@npm:0.60.0" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@oxfmt/binding-darwin-x64@npm:0.59.0": - version: 0.59.0 - resolution: "@oxfmt/binding-darwin-x64@npm:0.59.0" +"@oxfmt/binding-darwin-x64@npm:0.60.0": + version: 0.60.0 + resolution: "@oxfmt/binding-darwin-x64@npm:0.60.0" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@oxfmt/binding-freebsd-x64@npm:0.59.0": - version: 0.59.0 - resolution: "@oxfmt/binding-freebsd-x64@npm:0.59.0" +"@oxfmt/binding-freebsd-x64@npm:0.60.0": + version: 0.60.0 + resolution: "@oxfmt/binding-freebsd-x64@npm:0.60.0" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@oxfmt/binding-linux-arm-gnueabihf@npm:0.59.0": - version: 0.59.0 - resolution: "@oxfmt/binding-linux-arm-gnueabihf@npm:0.59.0" +"@oxfmt/binding-linux-arm-gnueabihf@npm:0.60.0": + version: 0.60.0 + resolution: "@oxfmt/binding-linux-arm-gnueabihf@npm:0.60.0" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@oxfmt/binding-linux-arm-musleabihf@npm:0.59.0": - version: 0.59.0 - resolution: "@oxfmt/binding-linux-arm-musleabihf@npm:0.59.0" +"@oxfmt/binding-linux-arm-musleabihf@npm:0.60.0": + version: 0.60.0 + resolution: "@oxfmt/binding-linux-arm-musleabihf@npm:0.60.0" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@oxfmt/binding-linux-arm64-gnu@npm:0.59.0": - version: 0.59.0 - resolution: "@oxfmt/binding-linux-arm64-gnu@npm:0.59.0" +"@oxfmt/binding-linux-arm64-gnu@npm:0.60.0": + version: 0.60.0 + resolution: "@oxfmt/binding-linux-arm64-gnu@npm:0.60.0" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@oxfmt/binding-linux-arm64-musl@npm:0.59.0": - version: 0.59.0 - resolution: "@oxfmt/binding-linux-arm64-musl@npm:0.59.0" +"@oxfmt/binding-linux-arm64-musl@npm:0.60.0": + version: 0.60.0 + resolution: "@oxfmt/binding-linux-arm64-musl@npm:0.60.0" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@oxfmt/binding-linux-ppc64-gnu@npm:0.59.0": - version: 0.59.0 - resolution: "@oxfmt/binding-linux-ppc64-gnu@npm:0.59.0" +"@oxfmt/binding-linux-ppc64-gnu@npm:0.60.0": + version: 0.60.0 + resolution: "@oxfmt/binding-linux-ppc64-gnu@npm:0.60.0" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@oxfmt/binding-linux-riscv64-gnu@npm:0.59.0": - version: 0.59.0 - resolution: "@oxfmt/binding-linux-riscv64-gnu@npm:0.59.0" +"@oxfmt/binding-linux-riscv64-gnu@npm:0.60.0": + version: 0.60.0 + resolution: "@oxfmt/binding-linux-riscv64-gnu@npm:0.60.0" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@oxfmt/binding-linux-riscv64-musl@npm:0.59.0": - version: 0.59.0 - resolution: "@oxfmt/binding-linux-riscv64-musl@npm:0.59.0" +"@oxfmt/binding-linux-riscv64-musl@npm:0.60.0": + version: 0.60.0 + resolution: "@oxfmt/binding-linux-riscv64-musl@npm:0.60.0" conditions: os=linux & cpu=riscv64 & libc=musl languageName: node linkType: hard -"@oxfmt/binding-linux-s390x-gnu@npm:0.59.0": - version: 0.59.0 - resolution: "@oxfmt/binding-linux-s390x-gnu@npm:0.59.0" +"@oxfmt/binding-linux-s390x-gnu@npm:0.60.0": + version: 0.60.0 + resolution: "@oxfmt/binding-linux-s390x-gnu@npm:0.60.0" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@oxfmt/binding-linux-x64-gnu@npm:0.59.0": - version: 0.59.0 - resolution: "@oxfmt/binding-linux-x64-gnu@npm:0.59.0" +"@oxfmt/binding-linux-x64-gnu@npm:0.60.0": + version: 0.60.0 + resolution: "@oxfmt/binding-linux-x64-gnu@npm:0.60.0" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@oxfmt/binding-linux-x64-musl@npm:0.59.0": - version: 0.59.0 - resolution: "@oxfmt/binding-linux-x64-musl@npm:0.59.0" +"@oxfmt/binding-linux-x64-musl@npm:0.60.0": + version: 0.60.0 + resolution: "@oxfmt/binding-linux-x64-musl@npm:0.60.0" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@oxfmt/binding-openharmony-arm64@npm:0.59.0": - version: 0.59.0 - resolution: "@oxfmt/binding-openharmony-arm64@npm:0.59.0" +"@oxfmt/binding-openharmony-arm64@npm:0.60.0": + version: 0.60.0 + resolution: "@oxfmt/binding-openharmony-arm64@npm:0.60.0" conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard -"@oxfmt/binding-win32-arm64-msvc@npm:0.59.0": - version: 0.59.0 - resolution: "@oxfmt/binding-win32-arm64-msvc@npm:0.59.0" +"@oxfmt/binding-win32-arm64-msvc@npm:0.60.0": + version: 0.60.0 + resolution: "@oxfmt/binding-win32-arm64-msvc@npm:0.60.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@oxfmt/binding-win32-ia32-msvc@npm:0.59.0": - version: 0.59.0 - resolution: "@oxfmt/binding-win32-ia32-msvc@npm:0.59.0" +"@oxfmt/binding-win32-ia32-msvc@npm:0.60.0": + version: 0.60.0 + resolution: "@oxfmt/binding-win32-ia32-msvc@npm:0.60.0" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@oxfmt/binding-win32-x64-msvc@npm:0.59.0": - version: 0.59.0 - resolution: "@oxfmt/binding-win32-x64-msvc@npm:0.59.0" +"@oxfmt/binding-win32-x64-msvc@npm:0.60.0": + version: 0.60.0 + resolution: "@oxfmt/binding-win32-x64-msvc@npm:0.60.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@oxlint/binding-android-arm-eabi@npm:1.74.0": - version: 1.74.0 - resolution: "@oxlint/binding-android-arm-eabi@npm:1.74.0" +"@oxlint/binding-android-arm-eabi@npm:1.75.0": + version: 1.75.0 + resolution: "@oxlint/binding-android-arm-eabi@npm:1.75.0" conditions: os=android & cpu=arm languageName: node linkType: hard -"@oxlint/binding-android-arm64@npm:1.74.0": - version: 1.74.0 - resolution: "@oxlint/binding-android-arm64@npm:1.74.0" +"@oxlint/binding-android-arm64@npm:1.75.0": + version: 1.75.0 + resolution: "@oxlint/binding-android-arm64@npm:1.75.0" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@oxlint/binding-darwin-arm64@npm:1.74.0": - version: 1.74.0 - resolution: "@oxlint/binding-darwin-arm64@npm:1.74.0" +"@oxlint/binding-darwin-arm64@npm:1.75.0": + version: 1.75.0 + resolution: "@oxlint/binding-darwin-arm64@npm:1.75.0" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@oxlint/binding-darwin-x64@npm:1.74.0": - version: 1.74.0 - resolution: "@oxlint/binding-darwin-x64@npm:1.74.0" +"@oxlint/binding-darwin-x64@npm:1.75.0": + version: 1.75.0 + resolution: "@oxlint/binding-darwin-x64@npm:1.75.0" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@oxlint/binding-freebsd-x64@npm:1.74.0": - version: 1.74.0 - resolution: "@oxlint/binding-freebsd-x64@npm:1.74.0" +"@oxlint/binding-freebsd-x64@npm:1.75.0": + version: 1.75.0 + resolution: "@oxlint/binding-freebsd-x64@npm:1.75.0" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@oxlint/binding-linux-arm-gnueabihf@npm:1.74.0": - version: 1.74.0 - resolution: "@oxlint/binding-linux-arm-gnueabihf@npm:1.74.0" +"@oxlint/binding-linux-arm-gnueabihf@npm:1.75.0": + version: 1.75.0 + resolution: "@oxlint/binding-linux-arm-gnueabihf@npm:1.75.0" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@oxlint/binding-linux-arm-musleabihf@npm:1.74.0": - version: 1.74.0 - resolution: "@oxlint/binding-linux-arm-musleabihf@npm:1.74.0" +"@oxlint/binding-linux-arm-musleabihf@npm:1.75.0": + version: 1.75.0 + resolution: "@oxlint/binding-linux-arm-musleabihf@npm:1.75.0" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@oxlint/binding-linux-arm64-gnu@npm:1.74.0": - version: 1.74.0 - resolution: "@oxlint/binding-linux-arm64-gnu@npm:1.74.0" +"@oxlint/binding-linux-arm64-gnu@npm:1.75.0": + version: 1.75.0 + resolution: "@oxlint/binding-linux-arm64-gnu@npm:1.75.0" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@oxlint/binding-linux-arm64-musl@npm:1.74.0": - version: 1.74.0 - resolution: "@oxlint/binding-linux-arm64-musl@npm:1.74.0" +"@oxlint/binding-linux-arm64-musl@npm:1.75.0": + version: 1.75.0 + resolution: "@oxlint/binding-linux-arm64-musl@npm:1.75.0" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@oxlint/binding-linux-ppc64-gnu@npm:1.74.0": - version: 1.74.0 - resolution: "@oxlint/binding-linux-ppc64-gnu@npm:1.74.0" +"@oxlint/binding-linux-ppc64-gnu@npm:1.75.0": + version: 1.75.0 + resolution: "@oxlint/binding-linux-ppc64-gnu@npm:1.75.0" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@oxlint/binding-linux-riscv64-gnu@npm:1.74.0": - version: 1.74.0 - resolution: "@oxlint/binding-linux-riscv64-gnu@npm:1.74.0" +"@oxlint/binding-linux-riscv64-gnu@npm:1.75.0": + version: 1.75.0 + resolution: "@oxlint/binding-linux-riscv64-gnu@npm:1.75.0" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@oxlint/binding-linux-riscv64-musl@npm:1.74.0": - version: 1.74.0 - resolution: "@oxlint/binding-linux-riscv64-musl@npm:1.74.0" +"@oxlint/binding-linux-riscv64-musl@npm:1.75.0": + version: 1.75.0 + resolution: "@oxlint/binding-linux-riscv64-musl@npm:1.75.0" conditions: os=linux & cpu=riscv64 & libc=musl languageName: node linkType: hard -"@oxlint/binding-linux-s390x-gnu@npm:1.74.0": - version: 1.74.0 - resolution: "@oxlint/binding-linux-s390x-gnu@npm:1.74.0" +"@oxlint/binding-linux-s390x-gnu@npm:1.75.0": + version: 1.75.0 + resolution: "@oxlint/binding-linux-s390x-gnu@npm:1.75.0" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@oxlint/binding-linux-x64-gnu@npm:1.74.0": - version: 1.74.0 - resolution: "@oxlint/binding-linux-x64-gnu@npm:1.74.0" +"@oxlint/binding-linux-x64-gnu@npm:1.75.0": + version: 1.75.0 + resolution: "@oxlint/binding-linux-x64-gnu@npm:1.75.0" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@oxlint/binding-linux-x64-musl@npm:1.74.0": - version: 1.74.0 - resolution: "@oxlint/binding-linux-x64-musl@npm:1.74.0" +"@oxlint/binding-linux-x64-musl@npm:1.75.0": + version: 1.75.0 + resolution: "@oxlint/binding-linux-x64-musl@npm:1.75.0" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@oxlint/binding-openharmony-arm64@npm:1.74.0": - version: 1.74.0 - resolution: "@oxlint/binding-openharmony-arm64@npm:1.74.0" +"@oxlint/binding-openharmony-arm64@npm:1.75.0": + version: 1.75.0 + resolution: "@oxlint/binding-openharmony-arm64@npm:1.75.0" conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard -"@oxlint/binding-win32-arm64-msvc@npm:1.74.0": - version: 1.74.0 - resolution: "@oxlint/binding-win32-arm64-msvc@npm:1.74.0" +"@oxlint/binding-win32-arm64-msvc@npm:1.75.0": + version: 1.75.0 + resolution: "@oxlint/binding-win32-arm64-msvc@npm:1.75.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@oxlint/binding-win32-ia32-msvc@npm:1.74.0": - version: 1.74.0 - resolution: "@oxlint/binding-win32-ia32-msvc@npm:1.74.0" +"@oxlint/binding-win32-ia32-msvc@npm:1.75.0": + version: 1.75.0 + resolution: "@oxlint/binding-win32-ia32-msvc@npm:1.75.0" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@oxlint/binding-win32-x64-msvc@npm:1.74.0": - version: 1.74.0 - resolution: "@oxlint/binding-win32-x64-msvc@npm:1.74.0" +"@oxlint/binding-win32-x64-msvc@npm:1.75.0": + version: 1.75.0 + resolution: "@oxlint/binding-win32-x64-msvc@npm:1.75.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -1345,25 +1397,29 @@ __metadata: version: 0.0.0-use.local resolution: "@webiny/stdlib@workspace:." dependencies: + "@11ty/gray-matter": "npm:^2.1.0" "@changesets/cli": "npm:^2.31.1" + "@modelcontextprotocol/sdk": "npm:^1.29.0" "@types/node": "npm:^26.1.1" "@vitest/coverage-v8": "npm:^4.1.10" "@webiny/di": "npm:^1.0.2" adio: "npm:^3.0.1" bson-objectid: "npm:^2.0.4" - dot-prop: "npm:^10.1.0" + dot-prop: "npm:^10.2.0" fast-glob: "npm:^3.3.3" - happy-dom: "npm:^20.11.0" + happy-dom: "npm:^20.11.1" nanoid: "npm:^6.0.0" nanoid-dictionary: "npm:^5.0.0" - oxfmt: "npm:^0.59.0" - oxlint: "npm:^1.74.0" + oxfmt: "npm:^0.60.0" + oxlint: "npm:^1.75.0" pino: "npm:^10.3.1" pino-pretty: "npm:^13.1.3" type-fest: "npm:^5.8.0" typescript: "npm:^7.0.2" vitest: "npm:^4.1.10" zod: "npm:^4.4.3" + bin: + stdlib-mcp: ./dist/mcp/cli.js languageName: unknown linkType: soft @@ -1374,6 +1430,16 @@ __metadata: languageName: node linkType: hard +"accepts@npm:^2.0.0": + version: 2.0.0 + resolution: "accepts@npm:2.0.0" + dependencies: + mime-types: "npm:^3.0.0" + negotiator: "npm:^1.0.0" + checksum: 10c0/98374742097e140891546076215f90c32644feacf652db48412329de4c2a529178a81aa500fbb13dd3e6cbf6e68d829037b123ac037fc9a08bcec4b87b358eef + languageName: node + linkType: hard + "adio@npm:^3.0.1": version: 3.0.1 resolution: "adio@npm:3.0.1" @@ -1388,6 +1454,32 @@ __metadata: languageName: node linkType: hard +"ajv-formats@npm:^3.0.1": + version: 3.0.1 + resolution: "ajv-formats@npm:3.0.1" + dependencies: + ajv: "npm:^8.0.0" + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + checksum: 10c0/168d6bca1ea9f163b41c8147bae537e67bd963357a5488a1eaf3abe8baa8eec806d4e45f15b10767e6020679315c7e1e5e6803088dfb84efa2b4e9353b83dd0a + languageName: node + linkType: hard + +"ajv@npm:^8.0.0, ajv@npm:^8.17.1": + version: 8.20.0 + resolution: "ajv@npm:8.20.0" + dependencies: + fast-deep-equal: "npm:^3.1.3" + fast-uri: "npm:^3.0.1" + json-schema-traverse: "npm:^1.0.0" + require-from-string: "npm:^2.0.2" + checksum: 10c0/5df9a1c8f83863cde1bd3a9ddb426f599718f88e3dc9153616c79fb28e0be455335830d7f21d745576519f057b371352daa31047b6a33d7036fe08777d60cf2a + languageName: node + linkType: hard + "ansi-colors@npm:^4.1.1, ansi-colors@npm:^4.1.3": version: 4.1.3 resolution: "ansi-colors@npm:4.1.3" @@ -1443,6 +1535,20 @@ __metadata: languageName: node linkType: hard +"async-function@npm:^1.0.0": + version: 1.0.0 + resolution: "async-function@npm:1.0.0" + checksum: 10c0/669a32c2cb7e45091330c680e92eaeb791bc1d4132d827591e499cd1f776ff5a873e77e5f92d0ce795a8d60f10761dec9ddfe7225a5de680f5d357f67b1aac73 + languageName: node + linkType: hard + +"async-generator-function@npm:^1.0.0": + version: 1.0.0 + resolution: "async-generator-function@npm:1.0.0" + checksum: 10c0/2c50ef856c543ad500d8d8777d347e3c1ba623b93e99c9263ecc5f965c1b12d2a140e2ab6e43c3d0b85366110696f28114649411cbcd10b452a92a2318394186 + languageName: node + linkType: hard + "atomic-sleep@npm:^1.0.0": version: 1.0.0 resolution: "atomic-sleep@npm:1.0.0" @@ -1466,12 +1572,29 @@ __metadata: languageName: node linkType: hard +"body-parser@npm:^2.2.1": + version: 2.3.0 + resolution: "body-parser@npm:2.3.0" + dependencies: + bytes: "npm:^3.1.2" + content-type: "npm:^2.0.0" + debug: "npm:^4.4.3" + http-errors: "npm:^2.0.1" + iconv-lite: "npm:^0.7.2" + on-finished: "npm:^2.4.1" + qs: "npm:^6.15.2" + raw-body: "npm:^3.0.2" + type-is: "npm:^2.1.0" + checksum: 10c0/2a8fbbdc471b588338555a3e1a597d1eb0ad0c21cf20fdc3bac5d3f8d9c3a4b19b4163575ab852a43a5dfc0df7770ced5284f979e561f1a24c2f851ce89e695a + languageName: node + linkType: hard + "brace-expansion@npm:^5.0.5": - version: 5.0.7 - resolution: "brace-expansion@npm:5.0.7" + version: 5.0.8 + resolution: "brace-expansion@npm:5.0.8" dependencies: balanced-match: "npm:^4.0.2" - checksum: 10c0/4769109c3c082de178e449a371bcad50d51ab468f644bce2dd9188efe0cf0a080ed102105d7fc8577382cedc45bad7e6443a91bf3d8102264ee8cf927dbaf205 + checksum: 10c0/73304caafd00fdc5b0168e693ac15bf25b6357dec76d1195fcd628fe2cf99c46f9c889a7ecfa3cfe236dbd2d5ce3e27a6ccc4370b46d475d0d19081e11f86019 languageName: node linkType: hard @@ -1500,6 +1623,33 @@ __metadata: languageName: node linkType: hard +"bytes@npm:^3.1.2, bytes@npm:~3.1.2": + version: 3.1.2 + resolution: "bytes@npm:3.1.2" + checksum: 10c0/76d1c43cbd602794ad8ad2ae94095cddeb1de78c5dddaa7005c51af10b0176c69971a6d88e805a90c2b6550d76636e43c40d8427a808b8645ede885de4a0358e + languageName: node + linkType: hard + +"call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": + version: 1.0.2 + resolution: "call-bind-apply-helpers@npm:1.0.2" + dependencies: + es-errors: "npm:^1.3.0" + function-bind: "npm:^1.1.2" + checksum: 10c0/47bd9901d57b857590431243fea704ff18078b16890a6b3e021e12d279bbf211d039155e27d7566b374d49ee1f8189344bac9833dec7a20cdec370506361c938 + languageName: node + linkType: hard + +"call-bound@npm:^1.0.2": + version: 1.0.4 + resolution: "call-bound@npm:1.0.4" + dependencies: + call-bind-apply-helpers: "npm:^1.0.2" + get-intrinsic: "npm:^1.3.0" + checksum: 10c0/f4796a6a0941e71c766aea672f63b72bc61234c4f4964dc6d7606e3664c307e7d77845328a8f3359ce39ddb377fed67318f9ee203dea1d47e46165dcf2917644 + languageName: node + linkType: hard + "callsites@npm:^3.0.0": version: 3.1.0 resolution: "callsites@npm:3.1.0" @@ -1542,6 +1692,27 @@ __metadata: languageName: node linkType: hard +"content-disposition@npm:^1.0.0": + version: 1.1.0 + resolution: "content-disposition@npm:1.1.0" + checksum: 10c0/94e0aef65873e69330f5f187fbc44ebce593bdcb8013dd8a68b7d0f159ca089bd30db3f8095d829f81c341695b60a6085ee6e15e6d775c4a325b586cc8d91974 + languageName: node + linkType: hard + +"content-type@npm:^1.0.5": + version: 1.0.5 + resolution: "content-type@npm:1.0.5" + checksum: 10c0/b76ebed15c000aee4678c3707e0860cb6abd4e680a598c0a26e17f0bfae723ec9cc2802f0ff1bc6e4d80603719010431d2231018373d4dde10f9ccff9dadf5af + languageName: node + linkType: hard + +"content-type@npm:^2.0.0": + version: 2.0.0 + resolution: "content-type@npm:2.0.0" + checksum: 10c0/491539fff707d7594b0ca4fabcc084bef2a31ffa754ff0a4f80c4377e3963cff0394317f9271c24087596c97fa675bc123d61fa34ffe65b4904e7d3d3098de72 + languageName: node + linkType: hard + "convert-source-map@npm:^2.0.0": version: 2.0.0 resolution: "convert-source-map@npm:2.0.0" @@ -1549,6 +1720,30 @@ __metadata: languageName: node linkType: hard +"cookie-signature@npm:^1.2.1": + version: 1.2.2 + resolution: "cookie-signature@npm:1.2.2" + checksum: 10c0/54e05df1a293b3ce81589b27dddc445f462f6fa6812147c033350cd3561a42bc14481674e05ed14c7bd0ce1e8bb3dc0e40851bad75415733711294ddce0b7bc6 + languageName: node + linkType: hard + +"cookie@npm:^0.7.1": + version: 0.7.2 + resolution: "cookie@npm:0.7.2" + checksum: 10c0/9596e8ccdbf1a3a88ae02cf5ee80c1c50959423e1022e4e60b91dd87c622af1da309253d8abdb258fb5e3eacb4f08e579dc58b4897b8087574eee0fd35dfa5d2 + languageName: node + linkType: hard + +"cors@npm:^2.8.5": + version: 2.8.6 + resolution: "cors@npm:2.8.6" + dependencies: + object-assign: "npm:^4" + vary: "npm:^1" + checksum: 10c0/ab2bc57b8af8ef8476682a59647f7c55c1a7d406b559ac06119aa1c5f70b96d35036864d197b24cf86e228e4547231088f1f94ca05061dbb14d89cc0bc9d4cab + languageName: node + linkType: hard + "cosmiconfig@npm:^9.0.1": version: 9.0.2 resolution: "cosmiconfig@npm:9.0.2" @@ -1584,6 +1779,25 @@ __metadata: languageName: node linkType: hard +"debug@npm:^4.4.0, debug@npm:^4.4.3": + version: 4.4.3 + resolution: "debug@npm:4.4.3" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10c0/d79136ec6c83ecbefd0f6a5593da6a9c91ec4d7ddc4b54c883d6e71ec9accb5f67a1a5e96d00a328196b5b5c86d365e98d8a3a70856aaf16b4e7b1985e67f5a6 + languageName: node + linkType: hard + +"depd@npm:^2.0.0, depd@npm:~2.0.0": + version: 2.0.0 + resolution: "depd@npm:2.0.0" + checksum: 10c0/58bd06ec20e19529b06f7ad07ddab60e504d9e0faca4bd23079fac2d279c3594334d736508dc350e06e510aba5e22e4594483b3a6562ce7c17dd797f4cc4ad2c + languageName: node + linkType: hard + "detect-indent@npm:^6.0.0": version: 6.1.0 resolution: "detect-indent@npm:6.1.0" @@ -1607,12 +1821,37 @@ __metadata: languageName: node linkType: hard -"dot-prop@npm:^10.1.0": - version: 10.1.0 - resolution: "dot-prop@npm:10.1.0" +"dot-prop@npm:^10.2.0": + version: 10.2.0 + resolution: "dot-prop@npm:10.2.0" dependencies: type-fest: "npm:^5.0.0" - checksum: 10c0/b034a06f017909ed55c6c164ddea962ccdce3d88b9b092f7106a9b738116a4cd003db5d47a0c6e140e93adbf1922b5e3a147e3d4a124c8556862940446ba5f75 + checksum: 10c0/171de909a3a06b524dcfc67b29d4e218784f7ceb79fe6d4dbef2c0cddee0eb1ac857faad4535ff3a55c947feb01dc99e695b68ea23079d2088955006e5f503cf + languageName: node + linkType: hard + +"dunder-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "dunder-proto@npm:1.0.1" + dependencies: + call-bind-apply-helpers: "npm:^1.0.1" + es-errors: "npm:^1.3.0" + gopd: "npm:^1.2.0" + checksum: 10c0/199f2a0c1c16593ca0a145dbf76a962f8033ce3129f01284d48c45ed4e14fea9bbacd7b3610b6cdc33486cef20385ac054948fefc6272fcce645c09468f93031 + languageName: node + linkType: hard + +"ee-first@npm:1.1.1": + version: 1.1.1 + resolution: "ee-first@npm:1.1.1" + checksum: 10c0/b5bb125ee93161bc16bfe6e56c6b04de5ad2aa44234d8f644813cc95d861a6910903132b05093706de2b706599367c4130eb6d170f6b46895686b95f87d017b7 + languageName: node + linkType: hard + +"encodeurl@npm:^2.0.0": + version: 2.0.0 + resolution: "encodeurl@npm:2.0.0" + checksum: 10c0/5d317306acb13e6590e28e27924c754163946a2480de11865c991a3a7eed4315cd3fba378b543ca145829569eefe9b899f3d84bb09870f675ae60bc924b01ceb languageName: node linkType: hard @@ -1658,6 +1897,20 @@ __metadata: languageName: node linkType: hard +"es-define-property@npm:^1.0.1": + version: 1.0.1 + resolution: "es-define-property@npm:1.0.1" + checksum: 10c0/3f54eb49c16c18707949ff25a1456728c883e81259f045003499efba399c08bad00deebf65cccde8c0e07908c1a225c9d472b7107e558f2a48e28d530e34527c + languageName: node + linkType: hard + +"es-errors@npm:^1.3.0": + version: 1.3.0 + resolution: "es-errors@npm:1.3.0" + checksum: 10c0/0a61325670072f98d8ae3b914edab3559b6caa980f08054a3b872052640d91da01d38df55df797fcc916389d77fc92b8d5906cf028f4db46d7e3003abecbca85 + languageName: node + linkType: hard + "es-module-lexer@npm:^2.0.0": version: 2.3.1 resolution: "es-module-lexer@npm:2.3.1" @@ -1665,6 +1918,22 @@ __metadata: languageName: node linkType: hard +"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1": + version: 1.1.2 + resolution: "es-object-atoms@npm:1.1.2" + dependencies: + es-errors: "npm:^1.3.0" + checksum: 10c0/1772861f094f739d6f41b579cfb9a18579daffeb434552a370a5fbef50a32d22227e27b63fdbb757b7ddd429d1b42fe52ccae7966d9302a2ec221b6f1b41bbc4 + languageName: node + linkType: hard + +"escape-html@npm:^1.0.3": + version: 1.0.3 + resolution: "escape-html@npm:1.0.3" + checksum: 10c0/524c739d776b36c3d29fa08a22e03e8824e3b2fd57500e5e44ecf3cc4707c34c60f9ca0781c0e33d191f2991161504c295e98f68c78fe7baa6e57081ec6ac0a3 + languageName: node + linkType: hard + "esprima@npm:^4.0.0": version: 4.0.1 resolution: "esprima@npm:4.0.1" @@ -1684,6 +1953,29 @@ __metadata: languageName: node linkType: hard +"etag@npm:^1.8.1": + version: 1.8.1 + resolution: "etag@npm:1.8.1" + checksum: 10c0/12be11ef62fb9817314d790089a0a49fae4e1b50594135dcb8076312b7d7e470884b5100d249b28c18581b7fd52f8b485689ffae22a11ed9ec17377a33a08f84 + languageName: node + linkType: hard + +"eventsource-parser@npm:^3.0.0, eventsource-parser@npm:^3.0.1": + version: 3.1.0 + resolution: "eventsource-parser@npm:3.1.0" + checksum: 10c0/5ab4c6c9a2a042be0b387b6d03810eb580bac4ce90e299ede56458125a97ffe3af8145b2740089fc898a96cfa5aae792ee79f2a06257fba2776b0e7bce037071 + languageName: node + linkType: hard + +"eventsource@npm:^3.0.2": + version: 3.0.7 + resolution: "eventsource@npm:3.0.7" + dependencies: + eventsource-parser: "npm:^3.0.1" + checksum: 10c0/c48a73c38f300e33e9f11375d4ee969f25cbb0519608a12378a38068055ae8b55b6e0e8a49c3f91c784068434efe1d9f01eb49b6315b04b0da9157879ce2f67d + languageName: node + linkType: hard + "expect-type@npm:^1.3.0": version: 1.4.0 resolution: "expect-type@npm:1.4.0" @@ -1698,6 +1990,63 @@ __metadata: languageName: node linkType: hard +"express-rate-limit@npm:^8.2.1": + version: 8.6.0 + resolution: "express-rate-limit@npm:8.6.0" + dependencies: + debug: "npm:^4.4.3" + ip-address: "npm:^10.2.0" + peerDependencies: + express: ">= 4.11" + checksum: 10c0/cce72e87963d90797ddced59de71046b90ca3ca088ab8e73587b50e812aaefa0b79f5a779743034f3ada01fd27d5c7f82063992a02981c7bc9ac74f8b01bb41a + languageName: node + linkType: hard + +"express@npm:^5.2.1": + version: 5.2.1 + resolution: "express@npm:5.2.1" + dependencies: + accepts: "npm:^2.0.0" + body-parser: "npm:^2.2.1" + content-disposition: "npm:^1.0.0" + content-type: "npm:^1.0.5" + cookie: "npm:^0.7.1" + cookie-signature: "npm:^1.2.1" + debug: "npm:^4.4.0" + depd: "npm:^2.0.0" + encodeurl: "npm:^2.0.0" + escape-html: "npm:^1.0.3" + etag: "npm:^1.8.1" + finalhandler: "npm:^2.1.0" + fresh: "npm:^2.0.0" + http-errors: "npm:^2.0.0" + merge-descriptors: "npm:^2.0.0" + mime-types: "npm:^3.0.0" + on-finished: "npm:^2.4.1" + once: "npm:^1.4.0" + parseurl: "npm:^1.3.3" + proxy-addr: "npm:^2.0.7" + qs: "npm:^6.14.0" + range-parser: "npm:^1.2.1" + router: "npm:^2.2.0" + send: "npm:^1.1.0" + serve-static: "npm:^2.2.0" + statuses: "npm:^2.0.1" + type-is: "npm:^2.0.1" + vary: "npm:^1.1.2" + checksum: 10c0/45e8c841ad188a41402ddcd1294901e861ee0819f632fb494f2ed344ef9c43315d294d443fb48d594e6586a3b779785120f43321417adaef8567316a55072949 + languageName: node + linkType: hard + +"extend-shallow@npm:^2.0.1": + version: 2.0.1 + resolution: "extend-shallow@npm:2.0.1" + dependencies: + is-extendable: "npm:^0.1.0" + checksum: 10c0/ee1cb0a18c9faddb42d791b2d64867bd6cfd0f3affb711782eb6e894dd193e2934a7f529426aac7c8ddb31ac5d38000a00aa2caf08aa3dfc3e1c8ff6ba340bd9 + languageName: node + linkType: hard + "extendable-error@npm:^0.1.5": version: 0.1.7 resolution: "extendable-error@npm:0.1.7" @@ -1712,6 +2061,13 @@ __metadata: languageName: node linkType: hard +"fast-deep-equal@npm:^3.1.3": + version: 3.1.3 + resolution: "fast-deep-equal@npm:3.1.3" + checksum: 10c0/40dedc862eb8992c54579c66d914635afbec43350afbbe991235fdcb4e3a8d5af1b23ae7e79bef7d4882d0ecee06c3197488026998fb19f72dc95acff1d1b1d0 + languageName: node + linkType: hard + "fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.3": version: 3.3.3 resolution: "fast-glob@npm:3.3.3" @@ -1732,6 +2088,13 @@ __metadata: languageName: node linkType: hard +"fast-uri@npm:^3.0.1": + version: 3.1.4 + resolution: "fast-uri@npm:3.1.4" + checksum: 10c0/f90948821ceb49980f64f89b8216ba498f5957f26035be813526a55b6145d26cbd63ef5618d5205a3292b31edc9c08589749350cd72bd86c7095eb434dceb757 + languageName: node + linkType: hard + "fastq@npm:^1.6.0": version: 1.20.1 resolution: "fastq@npm:1.20.1" @@ -1762,6 +2125,20 @@ __metadata: languageName: node linkType: hard +"finalhandler@npm:^2.1.0": + version: 2.1.1 + resolution: "finalhandler@npm:2.1.1" + dependencies: + debug: "npm:^4.4.0" + encodeurl: "npm:^2.0.0" + escape-html: "npm:^1.0.3" + on-finished: "npm:^2.4.1" + parseurl: "npm:^1.3.3" + statuses: "npm:^2.0.1" + checksum: 10c0/6bd664e21b7b2e79efcaace7d1a427169f61cce048fae68eb56290e6934e676b78e55d89f5998c5508871345bc59a61f47002dc505dc7288be68cceac1b701e2 + languageName: node + linkType: hard + "find-up@npm:^4.1.0": version: 4.1.0 resolution: "find-up@npm:4.1.0" @@ -1772,6 +2149,20 @@ __metadata: languageName: node linkType: hard +"forwarded@npm:0.2.0": + version: 0.2.0 + resolution: "forwarded@npm:0.2.0" + checksum: 10c0/9b67c3fac86acdbc9ae47ba1ddd5f2f81526fa4c8226863ede5600a3f7c7416ef451f6f1e240a3cc32d0fd79fcfe6beb08fd0da454f360032bde70bf80afbb33 + languageName: node + linkType: hard + +"fresh@npm:^2.0.0": + version: 2.0.0 + resolution: "fresh@npm:2.0.0" + checksum: 10c0/0557548194cb9a809a435bf92bcfbc20c89e8b5eb38861b73ced36750437251e39a111fc3a18b98531be9dd91fe1411e4969f229dc579ec0251ce6c5d4900bbc + languageName: node + linkType: hard + "fs-extra@npm:^7.0.1": version: 7.0.1 resolution: "fs-extra@npm:7.0.1" @@ -1813,6 +2204,51 @@ __metadata: languageName: node linkType: hard +"function-bind@npm:^1.1.2": + version: 1.1.2 + resolution: "function-bind@npm:1.1.2" + checksum: 10c0/d8680ee1e5fcd4c197e4ac33b2b4dce03c71f4d91717292785703db200f5c21f977c568d28061226f9b5900cbcd2c84463646134fd5337e7925e0942bc3f46d5 + languageName: node + linkType: hard + +"generator-function@npm:^2.0.0": + version: 2.0.1 + resolution: "generator-function@npm:2.0.1" + checksum: 10c0/8a9f59df0f01cfefafdb3b451b80555e5cf6d76487095db91ac461a0e682e4ff7a9dbce15f4ecec191e53586d59eece01949e05a4b4492879600bbbe8e28d6b8 + languageName: node + linkType: hard + +"get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.3.0": + version: 1.3.1 + resolution: "get-intrinsic@npm:1.3.1" + dependencies: + async-function: "npm:^1.0.0" + async-generator-function: "npm:^1.0.0" + call-bind-apply-helpers: "npm:^1.0.2" + es-define-property: "npm:^1.0.1" + es-errors: "npm:^1.3.0" + es-object-atoms: "npm:^1.1.1" + function-bind: "npm:^1.1.2" + generator-function: "npm:^2.0.0" + get-proto: "npm:^1.0.1" + gopd: "npm:^1.2.0" + has-symbols: "npm:^1.1.0" + hasown: "npm:^2.0.2" + math-intrinsics: "npm:^1.1.0" + checksum: 10c0/9f4ab0cf7efe0fd2c8185f52e6f637e708f3a112610c88869f8f041bb9ecc2ce44bf285dfdbdc6f4f7c277a5b88d8e94a432374d97cca22f3de7fc63795deb5d + languageName: node + linkType: hard + +"get-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "get-proto@npm:1.0.1" + dependencies: + dunder-proto: "npm:^1.0.1" + es-object-atoms: "npm:^1.0.0" + checksum: 10c0/9224acb44603c5526955e83510b9da41baf6ae73f7398875fba50edc5e944223a89c4a72b070fcd78beb5f7bdda58ecb6294adc28f7acfc0da05f76a2399643c + languageName: node + linkType: hard + "glob-parent@npm:^5.1.2": version: 5.1.2 resolution: "glob-parent@npm:5.1.2" @@ -1847,6 +2283,13 @@ __metadata: languageName: node linkType: hard +"gopd@npm:^1.2.0": + version: 1.2.0 + resolution: "gopd@npm:1.2.0" + checksum: 10c0/50fff1e04ba2b7737c097358534eacadad1e68d24cccee3272e04e007bed008e68d2614f3987788428fd192a5ae3889d08fb2331417e4fc4a9ab366b2043cead + languageName: node + linkType: hard + "graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.5, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.6": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" @@ -1854,9 +2297,9 @@ __metadata: languageName: node linkType: hard -"happy-dom@npm:^20.11.0": - version: 20.11.0 - resolution: "happy-dom@npm:20.11.0" +"happy-dom@npm:^20.11.1": + version: 20.11.1 + resolution: "happy-dom@npm:20.11.1" dependencies: "@types/node": "npm:>=20.0.0" "@types/whatwg-mimetype": "npm:^3.0.2" @@ -1865,7 +2308,7 @@ __metadata: entities: "npm:^7.0.1" whatwg-mimetype: "npm:^3.0.0" ws: "npm:^8.21.0" - checksum: 10c0/ef63674e3c3485c3793bbf4b18f842bc443cc53f430a36c278008fed6b2c873c14e29cd840d010565cc99038b851a47a566c96b984bcf534ab94ead099aa0529 + checksum: 10c0/f2335e8b87f1917c3725051c22dd7dafef13229af0dde9412d146fbaa961713a81ca4dcc741ecef8bf5fb64ea4182e3886305ac93ecb822dec7be028a6f7c51e languageName: node linkType: hard @@ -1876,6 +2319,22 @@ __metadata: languageName: node linkType: hard +"has-symbols@npm:^1.1.0": + version: 1.1.0 + resolution: "has-symbols@npm:1.1.0" + checksum: 10c0/dde0a734b17ae51e84b10986e651c664379018d10b91b6b0e9b293eddb32f0f069688c841fb40f19e9611546130153e0a2a48fd7f512891fb000ddfa36f5a20e + languageName: node + linkType: hard + +"hasown@npm:^2.0.2": + version: 2.0.4 + resolution: "hasown@npm:2.0.4" + dependencies: + function-bind: "npm:^1.1.2" + checksum: 10c0/2d8de939e270b70618f8cebb69746620db10617dbb495bc66ddad326955ea24d3ca4af133aff3eb7c1853e0218f867bc2b050ec26fe02e3aea58f880ffc5e506 + languageName: node + linkType: hard + "help-me@npm:^5.0.0": version: 5.0.0 resolution: "help-me@npm:5.0.0" @@ -1883,6 +2342,13 @@ __metadata: languageName: node linkType: hard +"hono@npm:^4.11.4": + version: 4.12.32 + resolution: "hono@npm:4.12.32" + checksum: 10c0/3bb2fc0b042af87202e0cb0e5f77ee5b578684d868e1e321ed3de53e02f1ef02fe700811fa0d2fc765636a217bae7633d552d55934175f3dec6307a1f63f4ecb + languageName: node + linkType: hard + "html-escaper@npm:^2.0.0": version: 2.0.2 resolution: "html-escaper@npm:2.0.2" @@ -1890,6 +2356,19 @@ __metadata: languageName: node linkType: hard +"http-errors@npm:^2.0.0, http-errors@npm:^2.0.1, http-errors@npm:~2.0.1": + version: 2.0.1 + resolution: "http-errors@npm:2.0.1" + dependencies: + depd: "npm:~2.0.0" + inherits: "npm:~2.0.4" + setprototypeof: "npm:~1.2.0" + statuses: "npm:~2.0.2" + toidentifier: "npm:~1.0.1" + checksum: 10c0/fb38906cef4f5c83952d97661fe14dc156cb59fe54812a42cd448fa57b5c5dfcb38a40a916957737bd6b87aab257c0648d63eb5b6a9ca9f548e105b6072712d4 + languageName: node + linkType: hard + "human-id@npm:^4.1.1": version: 4.2.0 resolution: "human-id@npm:4.2.0" @@ -1899,7 +2378,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:^0.7.0": +"iconv-lite@npm:^0.7.0, iconv-lite@npm:^0.7.2, iconv-lite@npm:~0.7.0": version: 0.7.3 resolution: "iconv-lite@npm:0.7.3" dependencies: @@ -1925,6 +2404,27 @@ __metadata: languageName: node linkType: hard +"inherits@npm:~2.0.4": + version: 2.0.4 + resolution: "inherits@npm:2.0.4" + checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2 + languageName: node + linkType: hard + +"ip-address@npm:^10.2.0": + version: 10.3.1 + resolution: "ip-address@npm:10.3.1" + checksum: 10c0/45b1c31e2cc53d6354ea39f276d70c365a9b0332e7ca2140a9ad4cdb7fdb9d73b6a7ec0d8bf19993d8dce7ab636cd86c46c3e417b98db5ccb8c25546fe8c0b17 + languageName: node + linkType: hard + +"ipaddr.js@npm:1.9.1": + version: 1.9.1 + resolution: "ipaddr.js@npm:1.9.1" + checksum: 10c0/0486e775047971d3fdb5fb4f063829bac45af299ae0b82dcf3afa2145338e08290563a2a70f34b732d795ecc8311902e541a8530eeb30d75860a78ff4e94ce2a + languageName: node + linkType: hard + "is-arrayish@npm:^0.2.1": version: 0.2.1 resolution: "is-arrayish@npm:0.2.1" @@ -1932,6 +2432,13 @@ __metadata: languageName: node linkType: hard +"is-extendable@npm:^0.1.0": + version: 0.1.1 + resolution: "is-extendable@npm:0.1.1" + checksum: 10c0/dd5ca3994a28e1740d1e25192e66eed128e0b2ff161a7ea348e87ae4f616554b486854de423877a2a2c171d5f7cd6e8093b91f54533bc88a59ee1c9838c43879 + languageName: node + linkType: hard + "is-extglob@npm:^2.1.1": version: 2.1.1 resolution: "is-extglob@npm:2.1.1" @@ -1955,6 +2462,13 @@ __metadata: languageName: node linkType: hard +"is-promise@npm:^4.0.0": + version: 4.0.0 + resolution: "is-promise@npm:4.0.0" + checksum: 10c0/ebd5c672d73db781ab33ccb155fb9969d6028e37414d609b115cc534654c91ccd061821d5b987eefaa97cf4c62f0b909bb2f04db88306de26e91bfe8ddc01503 + languageName: node + linkType: hard + "is-subdir@npm:^1.1.1": version: 1.2.0 resolution: "is-subdir@npm:1.2.0" @@ -2013,6 +2527,13 @@ __metadata: languageName: node linkType: hard +"jose@npm:^6.1.3": + version: 6.2.4 + resolution: "jose@npm:6.2.4" + checksum: 10c0/aaba866d71a8cd5bfd7c2c228367318df99f36318d7c6f1e7bca835c3bdd74ea3fe38783277f7bf7c9703ae24338bb3642a6ccdef6aa54c006d29a9711e6f782 + languageName: node + linkType: hard + "joycon@npm:^3.1.1": version: 3.1.1 resolution: "joycon@npm:3.1.1" @@ -2046,7 +2567,7 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:^4.1.0, js-yaml@npm:^4.1.1": +"js-yaml@npm:^4.1.0, js-yaml@npm:^4.1.1, js-yaml@npm:^4.2.0": version: 4.3.0 resolution: "js-yaml@npm:4.3.0" dependencies: @@ -2064,6 +2585,20 @@ __metadata: languageName: node linkType: hard +"json-schema-traverse@npm:^1.0.0": + version: 1.0.0 + resolution: "json-schema-traverse@npm:1.0.0" + checksum: 10c0/71e30015d7f3d6dc1c316d6298047c8ef98a06d31ad064919976583eb61e1018a60a0067338f0f79cabc00d84af3fcc489bd48ce8a46ea165d9541ba17fb30c6 + languageName: node + linkType: hard + +"json-schema-typed@npm:^8.0.2": + version: 8.0.2 + resolution: "json-schema-typed@npm:8.0.2" + checksum: 10c0/89f5e2fb1495483b705c027203c07277ee6bf2665165ad25a9cb55de5af7f72570326d13d32565180781e4083ad5c9688102f222baed7b353c2f39c1e02b0428 + languageName: node + linkType: hard + "jsonfile@npm:^4.0.0": version: 4.0.0 resolution: "jsonfile@npm:4.0.0" @@ -2076,6 +2611,13 @@ __metadata: languageName: node linkType: hard +"kind-of@npm:^6.0.0": + version: 6.0.3 + resolution: "kind-of@npm:6.0.3" + checksum: 10c0/61cdff9623dabf3568b6445e93e31376bee1cdb93f8ba7033d86022c2a9b1791a1d9510e026e6465ebd701a6dd2f7b0808483ad8838341ac52f003f512e0b4c4 + languageName: node + linkType: hard + "lightningcss-android-arm64@npm:1.33.0": version: 1.33.0 resolution: "lightningcss-android-arm64@npm:1.33.0" @@ -2255,6 +2797,27 @@ __metadata: languageName: node linkType: hard +"math-intrinsics@npm:^1.1.0": + version: 1.1.0 + resolution: "math-intrinsics@npm:1.1.0" + checksum: 10c0/7579ff94e899e2f76ab64491d76cf606274c874d8f2af4a442c016bd85688927fcfca157ba6bf74b08e9439dc010b248ce05b96cc7c126a354c3bae7fcb48b7f + languageName: node + linkType: hard + +"media-typer@npm:^1.1.0": + version: 1.1.1 + resolution: "media-typer@npm:1.1.1" + checksum: 10c0/924644de220c3107fc53ec0299b7c1488503470265d32f1492535bd798cc369ba7ceaf9c2a4533361590dcf7208d1cc7b7032dbc1a5918773f5f5d912cab5211 + languageName: node + linkType: hard + +"merge-descriptors@npm:^2.0.0": + version: 2.0.0 + resolution: "merge-descriptors@npm:2.0.0" + checksum: 10c0/95389b7ced3f9b36fbdcf32eb946dc3dd1774c2fdf164609e55b18d03aa499b12bd3aae3a76c1c7185b96279e9803525550d3eb292b5224866060a288f335cb3 + languageName: node + linkType: hard + "merge2@npm:^1.3.0, merge2@npm:^1.4.1": version: 1.4.1 resolution: "merge2@npm:1.4.1" @@ -2272,6 +2835,22 @@ __metadata: languageName: node linkType: hard +"mime-db@npm:^1.54.0": + version: 1.54.0 + resolution: "mime-db@npm:1.54.0" + checksum: 10c0/8d907917bc2a90fa2df842cdf5dfeaf509adc15fe0531e07bb2f6ab15992416479015828d6a74200041c492e42cce3ebf78e5ce714388a0a538ea9c53eece284 + languageName: node + linkType: hard + +"mime-types@npm:^3.0.0, mime-types@npm:^3.0.2": + version: 3.0.2 + resolution: "mime-types@npm:3.0.2" + dependencies: + mime-db: "npm:^1.54.0" + checksum: 10c0/35a0dd1035d14d185664f346efcdb72e93ef7a9b6e9ae808bd1f6358227010267fab52657b37562c80fc888ff76becb2b2938deb5e730818b7983bf8bd359767 + languageName: node + linkType: hard + "minimatch@npm:^10.2.2": version: 10.2.5 resolution: "minimatch@npm:10.2.5" @@ -2311,6 +2890,13 @@ __metadata: languageName: node linkType: hard +"ms@npm:^2.1.3": + version: 2.1.3 + resolution: "ms@npm:2.1.3" + checksum: 10c0/d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48 + languageName: node + linkType: hard + "nanoid-dictionary@npm:^5.0.0": version: 5.0.0 resolution: "nanoid-dictionary@npm:5.0.0" @@ -2336,6 +2922,13 @@ __metadata: languageName: node linkType: hard +"negotiator@npm:^1.0.0": + version: 1.0.0 + resolution: "negotiator@npm:1.0.0" + checksum: 10c0/4c559dd52669ea48e1914f9d634227c561221dd54734070791f999c52ed0ff36e437b2e07d5c1f6e32909fc625fe46491c16e4a8f0572567d4dd15c3a4fda04b + languageName: node + linkType: hard + "node-gyp@npm:latest": version: 13.0.1 resolution: "node-gyp@npm:13.0.1" @@ -2367,6 +2960,20 @@ __metadata: languageName: node linkType: hard +"object-assign@npm:^4": + version: 4.1.1 + resolution: "object-assign@npm:4.1.1" + checksum: 10c0/1f4df9945120325d041ccf7b86f31e8bcc14e73d29171e37a7903050e96b81323784ec59f93f102ec635bcf6fa8034ba3ea0a8c7e69fa202b87ae3b6cec5a414 + languageName: node + linkType: hard + +"object-inspect@npm:^1.13.3, object-inspect@npm:^1.13.4": + version: 1.13.4 + resolution: "object-inspect@npm:1.13.4" + checksum: 10c0/d7f8711e803b96ea3191c745d6f8056ce1f2496e530e6a19a0e92d89b0fa3c76d910c31f0aa270432db6bd3b2f85500a376a83aaba849a8d518c8845b3211692 + languageName: node + linkType: hard + "obug@npm:^2.1.1": version: 2.1.4 resolution: "obug@npm:2.1.4" @@ -2381,6 +2988,15 @@ __metadata: languageName: node linkType: hard +"on-finished@npm:^2.4.1": + version: 2.4.1 + resolution: "on-finished@npm:2.4.1" + dependencies: + ee-first: "npm:1.1.1" + checksum: 10c0/46fb11b9063782f2d9968863d9cbba33d77aa13c17f895f56129c274318b86500b22af3a160fe9995aa41317efcd22941b6eba747f718ced08d9a73afdb087b4 + languageName: node + linkType: hard + "once@npm:^1.3.1, once@npm:^1.4.0": version: 1.4.0 resolution: "once@npm:1.4.0" @@ -2467,29 +3083,29 @@ __metadata: languageName: node linkType: hard -"oxfmt@npm:^0.59.0": - version: 0.59.0 - resolution: "oxfmt@npm:0.59.0" - dependencies: - "@oxfmt/binding-android-arm-eabi": "npm:0.59.0" - "@oxfmt/binding-android-arm64": "npm:0.59.0" - "@oxfmt/binding-darwin-arm64": "npm:0.59.0" - "@oxfmt/binding-darwin-x64": "npm:0.59.0" - "@oxfmt/binding-freebsd-x64": "npm:0.59.0" - "@oxfmt/binding-linux-arm-gnueabihf": "npm:0.59.0" - "@oxfmt/binding-linux-arm-musleabihf": "npm:0.59.0" - "@oxfmt/binding-linux-arm64-gnu": "npm:0.59.0" - "@oxfmt/binding-linux-arm64-musl": "npm:0.59.0" - "@oxfmt/binding-linux-ppc64-gnu": "npm:0.59.0" - "@oxfmt/binding-linux-riscv64-gnu": "npm:0.59.0" - "@oxfmt/binding-linux-riscv64-musl": "npm:0.59.0" - "@oxfmt/binding-linux-s390x-gnu": "npm:0.59.0" - "@oxfmt/binding-linux-x64-gnu": "npm:0.59.0" - "@oxfmt/binding-linux-x64-musl": "npm:0.59.0" - "@oxfmt/binding-openharmony-arm64": "npm:0.59.0" - "@oxfmt/binding-win32-arm64-msvc": "npm:0.59.0" - "@oxfmt/binding-win32-ia32-msvc": "npm:0.59.0" - "@oxfmt/binding-win32-x64-msvc": "npm:0.59.0" +"oxfmt@npm:^0.60.0": + version: 0.60.0 + resolution: "oxfmt@npm:0.60.0" + dependencies: + "@oxfmt/binding-android-arm-eabi": "npm:0.60.0" + "@oxfmt/binding-android-arm64": "npm:0.60.0" + "@oxfmt/binding-darwin-arm64": "npm:0.60.0" + "@oxfmt/binding-darwin-x64": "npm:0.60.0" + "@oxfmt/binding-freebsd-x64": "npm:0.60.0" + "@oxfmt/binding-linux-arm-gnueabihf": "npm:0.60.0" + "@oxfmt/binding-linux-arm-musleabihf": "npm:0.60.0" + "@oxfmt/binding-linux-arm64-gnu": "npm:0.60.0" + "@oxfmt/binding-linux-arm64-musl": "npm:0.60.0" + "@oxfmt/binding-linux-ppc64-gnu": "npm:0.60.0" + "@oxfmt/binding-linux-riscv64-gnu": "npm:0.60.0" + "@oxfmt/binding-linux-riscv64-musl": "npm:0.60.0" + "@oxfmt/binding-linux-s390x-gnu": "npm:0.60.0" + "@oxfmt/binding-linux-x64-gnu": "npm:0.60.0" + "@oxfmt/binding-linux-x64-musl": "npm:0.60.0" + "@oxfmt/binding-openharmony-arm64": "npm:0.60.0" + "@oxfmt/binding-win32-arm64-msvc": "npm:0.60.0" + "@oxfmt/binding-win32-ia32-msvc": "npm:0.60.0" + "@oxfmt/binding-win32-x64-msvc": "npm:0.60.0" tinypool: "npm:2.1.0" peerDependencies: svelte: ^5.0.0 @@ -2540,35 +3156,35 @@ __metadata: optional: true bin: oxfmt: bin/oxfmt - checksum: 10c0/de40be77ee8cca8ea763e29d572eed59f508e48d76566035ba40d9f31ac99c19832c01fc8d62a64602506d508cefdb593eca7248b32186c001b20691b407eb04 - languageName: node - linkType: hard - -"oxlint@npm:^1.74.0": - version: 1.74.0 - resolution: "oxlint@npm:1.74.0" - dependencies: - "@oxlint/binding-android-arm-eabi": "npm:1.74.0" - "@oxlint/binding-android-arm64": "npm:1.74.0" - "@oxlint/binding-darwin-arm64": "npm:1.74.0" - "@oxlint/binding-darwin-x64": "npm:1.74.0" - "@oxlint/binding-freebsd-x64": "npm:1.74.0" - "@oxlint/binding-linux-arm-gnueabihf": "npm:1.74.0" - "@oxlint/binding-linux-arm-musleabihf": "npm:1.74.0" - "@oxlint/binding-linux-arm64-gnu": "npm:1.74.0" - "@oxlint/binding-linux-arm64-musl": "npm:1.74.0" - "@oxlint/binding-linux-ppc64-gnu": "npm:1.74.0" - "@oxlint/binding-linux-riscv64-gnu": "npm:1.74.0" - "@oxlint/binding-linux-riscv64-musl": "npm:1.74.0" - "@oxlint/binding-linux-s390x-gnu": "npm:1.74.0" - "@oxlint/binding-linux-x64-gnu": "npm:1.74.0" - "@oxlint/binding-linux-x64-musl": "npm:1.74.0" - "@oxlint/binding-openharmony-arm64": "npm:1.74.0" - "@oxlint/binding-win32-arm64-msvc": "npm:1.74.0" - "@oxlint/binding-win32-ia32-msvc": "npm:1.74.0" - "@oxlint/binding-win32-x64-msvc": "npm:1.74.0" + checksum: 10c0/44541bc1381964f75d2abf9cc6bbed868c1b4ddef7b4f6cd40a883c46e8b86de5208a4d3192875fc8decbce2a3c930970013455319ebeee9c126eb2afbf56229 + languageName: node + linkType: hard + +"oxlint@npm:^1.75.0": + version: 1.75.0 + resolution: "oxlint@npm:1.75.0" + dependencies: + "@oxlint/binding-android-arm-eabi": "npm:1.75.0" + "@oxlint/binding-android-arm64": "npm:1.75.0" + "@oxlint/binding-darwin-arm64": "npm:1.75.0" + "@oxlint/binding-darwin-x64": "npm:1.75.0" + "@oxlint/binding-freebsd-x64": "npm:1.75.0" + "@oxlint/binding-linux-arm-gnueabihf": "npm:1.75.0" + "@oxlint/binding-linux-arm-musleabihf": "npm:1.75.0" + "@oxlint/binding-linux-arm64-gnu": "npm:1.75.0" + "@oxlint/binding-linux-arm64-musl": "npm:1.75.0" + "@oxlint/binding-linux-ppc64-gnu": "npm:1.75.0" + "@oxlint/binding-linux-riscv64-gnu": "npm:1.75.0" + "@oxlint/binding-linux-riscv64-musl": "npm:1.75.0" + "@oxlint/binding-linux-s390x-gnu": "npm:1.75.0" + "@oxlint/binding-linux-x64-gnu": "npm:1.75.0" + "@oxlint/binding-linux-x64-musl": "npm:1.75.0" + "@oxlint/binding-openharmony-arm64": "npm:1.75.0" + "@oxlint/binding-win32-arm64-msvc": "npm:1.75.0" + "@oxlint/binding-win32-ia32-msvc": "npm:1.75.0" + "@oxlint/binding-win32-x64-msvc": "npm:1.75.0" peerDependencies: - oxlint-tsgolint: ">=0.24.0" + oxlint-tsgolint: ">=7.0.2001" vite-plus: "*" dependenciesMeta: "@oxlint/binding-android-arm-eabi": @@ -2616,7 +3232,7 @@ __metadata: optional: true bin: oxlint: bin/oxlint - checksum: 10c0/a2c9c3dd08ffd2eddf5c5b946f07c1d66184924a29b7ae0699e23bb8fd906e940a245e3e0b10ffb0c918327b7cef858f9a1fef09eb130ddb6b24fd2f42d703cb + checksum: 10c0/3cdd0d30ddd284d3c4caab9f9a9b0c71d18aab7fc00b115c37da95d5c648d8c62686c6aad3aab4633f25beb88e75f1f9bcc42dd66cedf7bdfacbfbe048e5dcf1 languageName: node linkType: hard @@ -2691,6 +3307,13 @@ __metadata: languageName: node linkType: hard +"parseurl@npm:^1.3.3": + version: 1.3.3 + resolution: "parseurl@npm:1.3.3" + checksum: 10c0/90dd4760d6f6174adb9f20cf0965ae12e23879b5f5464f38e92fce8073354341e4b3b76fa3d878351efe7d01e617121955284cfd002ab087fba1a0726ec0b4f5 + languageName: node + linkType: hard + "path-exists@npm:^4.0.0": version: 4.0.0 resolution: "path-exists@npm:4.0.0" @@ -2715,6 +3338,13 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:^8.0.0": + version: 8.4.2 + resolution: "path-to-regexp@npm:8.4.2" + checksum: 10c0/05b115c49b47ad252ce05faa32930f643f23769c68b8bcfe78ad833545140c48bbffb3266986d6c8d5db13a64cf12e07e0d72d9882cab830efeefa553533ebaf + languageName: node + linkType: hard + "path-type@npm:^4.0.0": version: 4.0.0 resolution: "path-type@npm:4.0.0" @@ -2817,14 +3447,21 @@ __metadata: languageName: node linkType: hard +"pkce-challenge@npm:^5.0.0": + version: 5.0.1 + resolution: "pkce-challenge@npm:5.0.1" + checksum: 10c0/207f4cb976682f27e8324eb49cf71937c98fbb8341a0b8f6142bc6f664825b30e049a54a21b5c034e823ee3c3d412f10d74bd21de78e17452a6a496c2991f57c + languageName: node + linkType: hard + "postcss@npm:^8.5.17": - version: 8.5.20 - resolution: "postcss@npm:8.5.20" + version: 8.5.23 + resolution: "postcss@npm:8.5.23" dependencies: nanoid: "npm:^3.3.16" picocolors: "npm:^1.1.1" source-map-js: "npm:^1.2.1" - checksum: 10c0/9c7a0e32c77ac54c5613f974e6ccc97d4a70fdb3bf242617f3fb4d652e5c62a37c86c589e6e7a2d1e7120f80bc169cdf673482b88bd75020d2a44899fa569c13 + checksum: 10c0/ed714e635b330bb42666ecdd0c0b4f30224ded15b0b0052d6bc2b92832f2cf17d1c1141359453f96f0a84f8fbf4c405a74df187f559163b8f31131b2535455aa languageName: node linkType: hard @@ -2851,6 +3488,16 @@ __metadata: languageName: node linkType: hard +"proxy-addr@npm:^2.0.7": + version: 2.0.7 + resolution: "proxy-addr@npm:2.0.7" + dependencies: + forwarded: "npm:0.2.0" + ipaddr.js: "npm:1.9.1" + checksum: 10c0/c3eed999781a35f7fd935f398b6d8920b6fb00bbc14287bc6de78128ccc1a02c89b95b56742bf7cf0362cc333c61d138532049c7dedc7a328ef13343eff81210 + languageName: node + linkType: hard + "pump@npm:^3.0.0": version: 3.0.4 resolution: "pump@npm:3.0.4" @@ -2861,6 +3508,16 @@ __metadata: languageName: node linkType: hard +"qs@npm:^6.14.0, qs@npm:^6.15.2": + version: 6.15.3 + resolution: "qs@npm:6.15.3" + dependencies: + es-define-property: "npm:^1.0.1" + side-channel: "npm:^1.1.1" + checksum: 10c0/8f3f6e45ece255347d57696628401cde29e9ec649fff698b53bd3150dea7cefdf33036e1bc1826b9f110bfa7cb0ec4ab9f5297eca628ce216c55af82c304e08e + languageName: node + linkType: hard + "quansync@npm:^0.2.7": version: 0.2.11 resolution: "quansync@npm:0.2.11" @@ -2882,6 +3539,25 @@ __metadata: languageName: node linkType: hard +"range-parser@npm:^1.2.1": + version: 1.3.0 + resolution: "range-parser@npm:1.3.0" + checksum: 10c0/295494bb6685f9ab50f41a5f4fa5ce445aeeb9673b491bf178460fcc3a812dcf0d164cf1c83074f13a71a87d91ead5e097afd53e0630bc49a5101ed60f2a7529 + languageName: node + linkType: hard + +"raw-body@npm:^3.0.0, raw-body@npm:^3.0.2": + version: 3.0.2 + resolution: "raw-body@npm:3.0.2" + dependencies: + bytes: "npm:~3.1.2" + http-errors: "npm:~2.0.1" + iconv-lite: "npm:~0.7.0" + unpipe: "npm:~1.0.0" + checksum: 10c0/d266678d08e1e7abea62c0ce5864344e980fa81c64f6b481e9842c5beaed2cdcf975f658a3ccd67ad35fc919c1f6664ccc106067801850286a6cbe101de89f29 + languageName: node + linkType: hard + "read-yaml-file@npm:^1.1.0": version: 1.1.0 resolution: "read-yaml-file@npm:1.1.0" @@ -2915,6 +3591,13 @@ __metadata: languageName: node linkType: hard +"require-from-string@npm:^2.0.2": + version: 2.0.2 + resolution: "require-from-string@npm:2.0.2" + checksum: 10c0/aaa267e0c5b022fc5fd4eef49d8285086b15f2a1c54b28240fdf03599cbd9c26049fee3eab894f2e1f6ca65e513b030a7c264201e3f005601e80c49fb2937ce2 + languageName: node + linkType: hard + "resolve-from@npm:^4.0.0": version: 4.0.0 resolution: "resolve-from@npm:4.0.0" @@ -2994,6 +3677,19 @@ __metadata: languageName: node linkType: hard +"router@npm:^2.2.0": + version: 2.2.0 + resolution: "router@npm:2.2.0" + dependencies: + debug: "npm:^4.4.0" + depd: "npm:^2.0.0" + is-promise: "npm:^4.0.0" + parseurl: "npm:^1.3.3" + path-to-regexp: "npm:^8.0.0" + checksum: 10c0/3279de7450c8eae2f6e095e9edacbdeec0abb5cb7249c6e719faa0db2dba43574b4fff5892d9220631c9abaff52dd3cad648cfea2aaace845e1a071915ac8867 + languageName: node + linkType: hard + "run-parallel@npm:^1.1.9": version: 1.2.0 resolution: "run-parallel@npm:1.2.0" @@ -3017,6 +3713,16 @@ __metadata: languageName: node linkType: hard +"section-matter@npm:^1.0.0": + version: 1.0.0 + resolution: "section-matter@npm:1.0.0" + dependencies: + extend-shallow: "npm:^2.0.1" + kind-of: "npm:^6.0.0" + checksum: 10c0/8007f91780adc5aaa781a848eaae50b0f680bbf4043b90cf8a96778195b8fab690c87fe7a989e02394ce69890e330811ec8dab22397d384673ce59f7d750641d + languageName: node + linkType: hard + "secure-json-parse@npm:^4.0.0": version: 4.1.0 resolution: "secure-json-parse@npm:4.1.0" @@ -3033,6 +3739,44 @@ __metadata: languageName: node linkType: hard +"send@npm:^1.1.0, send@npm:^1.2.0": + version: 1.2.1 + resolution: "send@npm:1.2.1" + dependencies: + debug: "npm:^4.4.3" + encodeurl: "npm:^2.0.0" + escape-html: "npm:^1.0.3" + etag: "npm:^1.8.1" + fresh: "npm:^2.0.0" + http-errors: "npm:^2.0.1" + mime-types: "npm:^3.0.2" + ms: "npm:^2.1.3" + on-finished: "npm:^2.4.1" + range-parser: "npm:^1.2.1" + statuses: "npm:^2.0.2" + checksum: 10c0/fbbbbdc902a913d65605274be23f3d604065cfc3ee3d78bf9fc8af1dc9fc82667c50d3d657f5e601ac657bac9b396b50ee97bd29cd55436320cf1cddebdcec72 + languageName: node + linkType: hard + +"serve-static@npm:^2.2.0": + version: 2.2.1 + resolution: "serve-static@npm:2.2.1" + dependencies: + encodeurl: "npm:^2.0.0" + escape-html: "npm:^1.0.3" + parseurl: "npm:^1.3.3" + send: "npm:^1.2.0" + checksum: 10c0/37986096e8572e2dfaad35a3925fa8da0c0969f8814fd7788e84d4d388bc068cf0c06d1658509788e55bed942a6b6d040a8a267fa92bb9ffb1179f8bacde5fd7 + languageName: node + linkType: hard + +"setprototypeof@npm:~1.2.0": + version: 1.2.0 + resolution: "setprototypeof@npm:1.2.0" + checksum: 10c0/68733173026766fa0d9ecaeb07f0483f4c2dc70ca376b3b7c40b7cda909f94b0918f6c5ad5ce27a9160bdfb475efaa9d5e705a11d8eaae18f9835d20976028bc + languageName: node + linkType: hard + "shebang-command@npm:^2.0.0": version: 2.0.0 resolution: "shebang-command@npm:2.0.0" @@ -3049,6 +3793,54 @@ __metadata: languageName: node linkType: hard +"side-channel-list@npm:^1.0.1": + version: 1.0.1 + resolution: "side-channel-list@npm:1.0.1" + dependencies: + es-errors: "npm:^1.3.0" + object-inspect: "npm:^1.13.4" + checksum: 10c0/d346c787fd2f9f1c2fdea14f00e8250118db0e7596d85a6cb9faa75f105d31a73a8f7a341c93d7df2a2429098c3d37a77bd3be9e88c37094b8c01807bc77c7a2 + languageName: node + linkType: hard + +"side-channel-map@npm:^1.0.1": + version: 1.0.1 + resolution: "side-channel-map@npm:1.0.1" + dependencies: + call-bound: "npm:^1.0.2" + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.5" + object-inspect: "npm:^1.13.3" + checksum: 10c0/010584e6444dd8a20b85bc926d934424bd809e1a3af941cace229f7fdcb751aada0fb7164f60c2e22292b7fa3c0ff0bce237081fd4cdbc80de1dc68e95430672 + languageName: node + linkType: hard + +"side-channel-weakmap@npm:^1.0.2": + version: 1.0.2 + resolution: "side-channel-weakmap@npm:1.0.2" + dependencies: + call-bound: "npm:^1.0.2" + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.5" + object-inspect: "npm:^1.13.3" + side-channel-map: "npm:^1.0.1" + checksum: 10c0/71362709ac233e08807ccd980101c3e2d7efe849edc51455030327b059f6c4d292c237f94dc0685031dd11c07dd17a68afde235d6cf2102d949567f98ab58185 + languageName: node + linkType: hard + +"side-channel@npm:^1.1.1": + version: 1.1.1 + resolution: "side-channel@npm:1.1.1" + dependencies: + es-errors: "npm:^1.3.0" + object-inspect: "npm:^1.13.4" + side-channel-list: "npm:^1.0.1" + side-channel-map: "npm:^1.0.1" + side-channel-weakmap: "npm:^1.0.2" + checksum: 10c0/dc0ab81d67f61bda9247d053ce93f41c3fd8ad2bdcb9cf9d8d2f8540d488f26d87a5e99ebfc07eea49ec025867b2452b705442d974b1478f0395e69f6bfb3270 + languageName: node + linkType: hard + "siginfo@npm:^2.0.0": version: 2.0.0 resolution: "siginfo@npm:2.0.0" @@ -3117,6 +3909,13 @@ __metadata: languageName: node linkType: hard +"statuses@npm:^2.0.1, statuses@npm:^2.0.2, statuses@npm:~2.0.2": + version: 2.0.2 + resolution: "statuses@npm:2.0.2" + checksum: 10c0/a9947d98ad60d01f6b26727570f3bcceb6c8fa789da64fe6889908fe2e294d57503b14bf2b5af7605c2d36647259e856635cd4c49eab41667658ec9d0080ec3f + languageName: node + linkType: hard + "std-env@npm:^4.0.0-rc.1": version: 4.2.0 resolution: "std-env@npm:4.2.0" @@ -3164,15 +3963,15 @@ __metadata: linkType: hard "tar@npm:^7.5.4": - version: 7.5.20 - resolution: "tar@npm:7.5.20" + version: 7.5.22 + resolution: "tar@npm:7.5.22" dependencies: "@isaacs/fs-minipass": "npm:^4.0.0" chownr: "npm:^3.0.0" minipass: "npm:^7.1.2" minizlib: "npm:^3.1.0" yallist: "npm:^5.0.0" - checksum: 10c0/4df4335c6d958b76adf1eaced55889dec3ca1c51f450658a074af80694bb0d9c154a8e93fdf4da617372d1575b121295379993961bbe4cd4b0867c0e5689846a + checksum: 10c0/1311f6be85a8157ac4c9147bae43e13923d2a1aae15e4aa1bd5239e4e03d2cf53cfe103dde7f35832fbb4c938b042856bc8e9a0afd29abd05e2d1608788c4fea languageName: node linkType: hard @@ -3239,6 +4038,13 @@ __metadata: languageName: node linkType: hard +"toidentifier@npm:~1.0.1": + version: 1.0.1 + resolution: "toidentifier@npm:1.0.1" + checksum: 10c0/93937279934bd66cc3270016dd8d0afec14fb7c94a05c72dc57321f8bd1fa97e5bea6d1f7c89e728d077ca31ea125b78320a616a6c6cd0e6b9cb94cb864381c1 + languageName: node + linkType: hard + "tslib@npm:^2.4.0": version: 2.8.1 resolution: "tslib@npm:2.8.1" @@ -3255,6 +4061,17 @@ __metadata: languageName: node linkType: hard +"type-is@npm:^2.0.1, type-is@npm:^2.1.0": + version: 2.1.0 + resolution: "type-is@npm:2.1.0" + dependencies: + content-type: "npm:^2.0.0" + media-typer: "npm:^1.1.0" + mime-types: "npm:^3.0.0" + checksum: 10c0/a6018f8f509de48f2c7429305e3a920e73b374fa93127dd0877ae1c2df65a5d33907caac8afb0c37a9b9fc7c49f29e3f55d668963dc845d966930b667c07f50e + languageName: node + linkType: hard + "typescript@npm:^7.0.2": version: 7.0.2 resolution: "typescript@npm:7.0.2" @@ -3405,9 +4222,9 @@ __metadata: linkType: hard "undici@npm:^8.4.1": - version: 8.7.0 - resolution: "undici@npm:8.7.0" - checksum: 10c0/b7e5ecb4de82fa4f905011a77544fe7ba4da06f27167ff99313a7ae2869f3cb233d676dcd085533bc69f36989bc40871f5ae6ea6e4c9b91db92616b9e91b579d + version: 8.9.0 + resolution: "undici@npm:8.9.0" + checksum: 10c0/e3d9fa35a9aa8360d9f56e66bd372f451ee053e25066a97fd9fab3816267035660f67870c6d709a58a5411551657af78c4475be5b31c113b04f70251309900d0 languageName: node linkType: hard @@ -3418,6 +4235,20 @@ __metadata: languageName: node linkType: hard +"unpipe@npm:~1.0.0": + version: 1.0.0 + resolution: "unpipe@npm:1.0.0" + checksum: 10c0/193400255bd48968e5c5383730344fbb4fa114cdedfab26e329e50dd2d81b134244bb8a72c6ac1b10ab0281a58b363d06405632c9d49ca9dfd5e90cbd7d0f32c + languageName: node + linkType: hard + +"vary@npm:^1, vary@npm:^1.1.2": + version: 1.1.2 + resolution: "vary@npm:1.1.2" + checksum: 10c0/f15d588d79f3675135ba783c91a4083dcd290a2a5be9fcb6514220a1634e23df116847b1cc51f66bfb0644cf9353b2abb7815ae499bab06e46dd33c1a6bf1f4f + languageName: node + linkType: hard + "vite@npm:^6.0.0 || ^7.0.0 || ^8.0.0": version: 8.1.5 resolution: "vite@npm:8.1.5" @@ -3613,7 +4444,16 @@ __metadata: languageName: node linkType: hard -"zod@npm:^4.4.3": +"zod-to-json-schema@npm:^3.25.1": + version: 3.25.2 + resolution: "zod-to-json-schema@npm:3.25.2" + peerDependencies: + zod: ^3.25.28 || ^4 + checksum: 10c0/dd300554393903022487688af14fbda5c719ba8179702bb55b3aa86318830467f0f7beb7d654036975ac963dc4843b72e59636448bfff9a0608f277bb6a14939 + languageName: node + linkType: hard + +"zod@npm:^3.25 || ^4.0, zod@npm:^4.4.3": version: 4.4.3 resolution: "zod@npm:4.4.3" checksum: 10c0/7ea31b558e88f9faf44f31dd185e2e1cbf51fed3081787fb96cc2534749b50c0acfc6da7f0922a7353ed092dd358c7d50c28ea96c94d04af64191bd33152eca3