add bmad-mcp package
This commit is contained in:
parent
ffae072143
commit
216f80af32
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
coverage
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"printWidth": 100,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"quoteProps": "as-needed",
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"arrowParens": "always",
|
||||||
|
"proseWrap": "preserve",
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.md",
|
||||||
|
"options": {
|
||||||
|
"proseWrap": "preserve",
|
||||||
|
"printWidth": 120
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
# BMAD-MCP
|
||||||
|
|
||||||
|
A Model Context Protocol (MCP) server that provides prompts and resources to support AI agents in development using the BMAD (Breakthrough Method of Agile AI-Driven Development) methodology.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Installation depends on your AI agent. For example in VSCode, you can install the npm package directly via action `MCP: Add Server...` → `NPM Package` → `@bmad/mcp-server`. Alternatively, you can install it as `stdio` MCP server that runs the command `npx @bmad/mcp-server`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
How to execute MCP prompts of this MCP server depends on your AI agent. For example in VSCode Copilot, you can show all available actions by typing `/` into the chat message field.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone <repository-url>
|
||||||
|
cd BMAD-METHOD/bmad-mcp
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Build the project
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
npm run test
|
||||||
|
|
||||||
|
# Start the MCP Inspector
|
||||||
|
npm run inspector
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install local build as MCP server
|
||||||
|
|
||||||
|
To install the local build during development, you can add `node bmad-mcp/dist/index.js` as MCP server. For example in VSCode, this can be done with action `MCP: Add Server...` → `Command (stdio)` → `node bmad-mcp/dist/index.js` (might need absolute path when not installing in workspace).
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Node.js 18+
|
||||||
|
- Compatible with MCP specification version `2025-03-26`
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,45 @@
|
||||||
|
{
|
||||||
|
"name": "@bmad/mcp-server",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"module": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"bin": {
|
||||||
|
"bmad-mcp": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite build --watch",
|
||||||
|
"build": "vite build",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"test": "vitest --run",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"test:coverage": "vitest run --coverage",
|
||||||
|
"test:ui": "vitest --ui",
|
||||||
|
"type-check": "tsc --noEmit",
|
||||||
|
"inspector": "npx @modelcontextprotocol/inspector node dist/index.js"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
"@types/node": "^22.10.1",
|
||||||
|
"@vitest/coverage-v8": "^3.2.4",
|
||||||
|
"@vitest/ui": "^3.2.4",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"vite": "^7.0.0",
|
||||||
|
"vite-plugin-dts": "^4.5.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.13.1",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
|
"vitest": "^3.2.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { BmadMcpServer } from "./server.js";
|
||||||
|
|
||||||
|
const server = new BmadMcpServer();
|
||||||
|
server.start().catch(console.error);
|
||||||
|
|
@ -0,0 +1,227 @@
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import { join, resolve, relative, extname, basename, dirname } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import type {
|
||||||
|
BaseResource,
|
||||||
|
ResourceProvider as IResourceProvider,
|
||||||
|
ResourceConfig,
|
||||||
|
} from "../types/index.js";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
export class ResourceProvider<T extends BaseResource> implements IResourceProvider<T> {
|
||||||
|
protected cache = new Map<string, T>();
|
||||||
|
protected metadataCache: T[] | null = null;
|
||||||
|
protected readonly resourceDir: string;
|
||||||
|
protected readonly directoryName: string;
|
||||||
|
protected supportedExtensions: string[];
|
||||||
|
protected config: ResourceConfig;
|
||||||
|
|
||||||
|
constructor(config: ResourceConfig, baseDir?: string) {
|
||||||
|
this.config = config;
|
||||||
|
this.directoryName = config.directory;
|
||||||
|
this.supportedExtensions = config.supportedExtensions;
|
||||||
|
|
||||||
|
if (baseDir) {
|
||||||
|
this.resourceDir = resolve(baseDir, `bmad-core/${this.directoryName}`);
|
||||||
|
} else {
|
||||||
|
this.resourceDir = resolve(__dirname, `bmad-core/${this.directoryName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discover all resources by scanning the directory recursively
|
||||||
|
*/
|
||||||
|
async discover(): Promise<T[]> {
|
||||||
|
if (this.metadataCache) {
|
||||||
|
return this.metadataCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resources = await this.scanResourceDirectory(this.resourceDir);
|
||||||
|
this.metadataCache = resources;
|
||||||
|
return resources;
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||||
|
console.warn(`${this.directoryName} directory not found: ${this.resourceDir}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
throw new Error(`Failed to discover ${this.directoryName}: ${(error as Error).message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific resource by ID, loading content if not cached
|
||||||
|
*/
|
||||||
|
async get(id: string): Promise<T> {
|
||||||
|
// Check cache first
|
||||||
|
if (this.cache.has(id)) {
|
||||||
|
return this.cache.get(id)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discover resources if not already cached
|
||||||
|
const resources = await this.discover();
|
||||||
|
const resource = resources.find((r) => r.id === id);
|
||||||
|
|
||||||
|
if (!resource) {
|
||||||
|
throw new Error(
|
||||||
|
`${
|
||||||
|
this.directoryName.slice(0, -1).charAt(0).toUpperCase() + this.directoryName.slice(1, -1)
|
||||||
|
} with ID '${id}' not found`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load content
|
||||||
|
const resourceWithContent = await this.loadResourceContent(resource);
|
||||||
|
|
||||||
|
// Cache it
|
||||||
|
this.cache.set(id, resourceWithContent);
|
||||||
|
|
||||||
|
return resourceWithContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discover all resources by scanning the directory recursively
|
||||||
|
*/
|
||||||
|
async discoverResources(): Promise<T[]> {
|
||||||
|
return await this.discover();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific resource by ID, loading content if not cached
|
||||||
|
*/
|
||||||
|
async getResource(id: string): Promise<T> {
|
||||||
|
return await this.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all resources with their content loaded
|
||||||
|
*/
|
||||||
|
async getAllResources(): Promise<T[]> {
|
||||||
|
const resources = await this.discoverResources();
|
||||||
|
const resourcesWithContent: T[] = [];
|
||||||
|
|
||||||
|
for (const resource of resources) {
|
||||||
|
try {
|
||||||
|
const resourceWithContent = await this.getResource(resource.id);
|
||||||
|
resourcesWithContent.push(resourceWithContent);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to load resource ${resource.id}:`, error);
|
||||||
|
// Include resource without content rather than failing completely
|
||||||
|
resourcesWithContent.push(resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resourcesWithContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scan directory recursively for resources
|
||||||
|
*/
|
||||||
|
private async scanResourceDirectory(dir: string): Promise<T[]> {
|
||||||
|
const resources: T[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = join(dir, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
// Recursively scan subdirectories
|
||||||
|
const subResources = await this.scanResourceDirectory(fullPath);
|
||||||
|
resources.push(...subResources);
|
||||||
|
} else if (entry.isFile()) {
|
||||||
|
// Process files
|
||||||
|
const resource = await this.createResourceFromFile(fullPath);
|
||||||
|
if (resource) {
|
||||||
|
resources.push(resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error scanning directory ${dir}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a resource metadata object from a file path
|
||||||
|
*/
|
||||||
|
private async createResourceFromFile(filePath: string): Promise<T | null> {
|
||||||
|
try {
|
||||||
|
const extension = extname(filePath).slice(1); // Remove the dot
|
||||||
|
const name = basename(filePath, `.${extension}`);
|
||||||
|
|
||||||
|
// Check if this file type should be processed
|
||||||
|
if (!this.isValidFile(filePath)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create relative path from the base directory
|
||||||
|
const relativePath = relative(this.resourceDir, filePath);
|
||||||
|
const id = relativePath.replace(/\\/g, "/").replace(`.${extension}`, "").replace(/\//g, "_");
|
||||||
|
|
||||||
|
const baseResource: BaseResource = {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
path: filePath,
|
||||||
|
extension,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.enhanceResource(baseResource);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error creating resource from file ${filePath}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load content for a resource
|
||||||
|
*/
|
||||||
|
private async loadResourceContent(resource: T): Promise<T> {
|
||||||
|
try {
|
||||||
|
const content = await fs.readFile(resource.path, "utf-8");
|
||||||
|
return {
|
||||||
|
...resource,
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error loading content for resource ${resource.id}:`, error);
|
||||||
|
throw new Error(
|
||||||
|
`Failed to load ${this.directoryName.slice(0, -1)} '${resource.id}': ${
|
||||||
|
(error as Error).message
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear caches
|
||||||
|
*/
|
||||||
|
clearCache(): void {
|
||||||
|
this.cache.clear();
|
||||||
|
this.metadataCache = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enhance the base resource using configuration
|
||||||
|
*/
|
||||||
|
protected enhanceResource(baseResource: BaseResource): T {
|
||||||
|
if (this.config.enhanceResource) {
|
||||||
|
return this.config.enhanceResource(baseResource) as T;
|
||||||
|
}
|
||||||
|
return baseResource as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if file is a supported format
|
||||||
|
*/
|
||||||
|
protected isValidFile(filePath: string): boolean {
|
||||||
|
const ext = extname(filePath).toLowerCase();
|
||||||
|
return this.supportedExtensions.includes(ext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,423 @@
|
||||||
|
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
|
import { ResourceProvider } from "./providers/resourceProvider.js";
|
||||||
|
import type {
|
||||||
|
ReadResourceResult,
|
||||||
|
ListResourcesResult,
|
||||||
|
Resource,
|
||||||
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
import type { Variables } from "@modelcontextprotocol/sdk/shared/uriTemplate.js";
|
||||||
|
import type { BaseResource, PromptConfig, ResourceConfig } from "./types/index.js";
|
||||||
|
import { load } from "js-yaml";
|
||||||
|
|
||||||
|
export class BmadMcpServer {
|
||||||
|
private server: McpServer;
|
||||||
|
private transport: StdioServerTransport;
|
||||||
|
private resourceProviders: Map<string, ResourceProvider<BaseResource>>;
|
||||||
|
|
||||||
|
private readonly agentsResourceConfig: ResourceConfig = {
|
||||||
|
resourceType: "Agent",
|
||||||
|
uriScheme: "bmad://agents",
|
||||||
|
directory: "agents",
|
||||||
|
nameSuffix: "",
|
||||||
|
description: "Individual BMAD agent content",
|
||||||
|
supportedExtensions: [".md"],
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO some bmad files contain instructions that are only relevant when using bmad-core as files and not as MCP resources, e.g. `IDE-FILE-RESOLUTION` in agent files - either change this in BMAD, or do some file content replacement here when serving resources
|
||||||
|
private readonly resourceConfigs: ResourceConfig[] = [
|
||||||
|
this.agentsResourceConfig,
|
||||||
|
{
|
||||||
|
resourceType: "Checklist",
|
||||||
|
uriScheme: "bmad://checklists",
|
||||||
|
directory: "checklists",
|
||||||
|
nameSuffix: "-checklist",
|
||||||
|
description: "Individual BMAD checklist content",
|
||||||
|
supportedExtensions: [".md"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resourceType: "Data",
|
||||||
|
uriScheme: "bmad://data",
|
||||||
|
directory: "data",
|
||||||
|
nameSuffix: "",
|
||||||
|
description: "Individual BMAD data content",
|
||||||
|
supportedExtensions: [".md"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resourceType: "Task",
|
||||||
|
uriScheme: "bmad://tasks",
|
||||||
|
directory: "tasks",
|
||||||
|
nameSuffix: "",
|
||||||
|
description: "Individual BMAD task content",
|
||||||
|
supportedExtensions: [".md"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resourceType: "Template",
|
||||||
|
uriScheme: "bmad://templates",
|
||||||
|
directory: "templates",
|
||||||
|
nameSuffix: "-tmpl",
|
||||||
|
description: "Individual BMAD template content",
|
||||||
|
supportedExtensions: [".md"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resourceType: "Util",
|
||||||
|
uriScheme: "bmad://utils",
|
||||||
|
directory: "utils",
|
||||||
|
nameSuffix: "",
|
||||||
|
description: "Individual BMAD util content",
|
||||||
|
supportedExtensions: [".md"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resourceType: "Workflow",
|
||||||
|
uriScheme: "bmad://workflows",
|
||||||
|
directory: "workflows",
|
||||||
|
nameSuffix: "",
|
||||||
|
description: "Individual BMAD workflow content",
|
||||||
|
supportedExtensions: [".yml"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
private readonly promptConfigs: PromptConfig[] = [
|
||||||
|
{
|
||||||
|
referencedResourceConfig: this.agentsResourceConfig,
|
||||||
|
parseDependencies: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.server = new McpServer(
|
||||||
|
{
|
||||||
|
name: "bmad-mcp",
|
||||||
|
version: "0.1.0",
|
||||||
|
description: "BMAD Method MCP Server",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
logging: {},
|
||||||
|
prompts: {},
|
||||||
|
resources: {},
|
||||||
|
},
|
||||||
|
instructions: `This MCP server provides prompts and resources to provide an Agile Development Framework. Call the bmad-orchestrator prompt to get started, or directly use the other prompts for PRD, Architecture, and Story creation and development.`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.transport = new StdioServerTransport(process.stdin, process.stdout);
|
||||||
|
this.resourceProviders = new Map();
|
||||||
|
|
||||||
|
for (const config of this.resourceConfigs) {
|
||||||
|
this.resourceProviders.set(config.resourceType, new ResourceProvider(config));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupResources() {
|
||||||
|
for (const config of this.resourceConfigs) {
|
||||||
|
this.setupResourceType(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupResourceType(config: ResourceConfig) {
|
||||||
|
const provider = this.resourceProviders.get(config.resourceType);
|
||||||
|
if (!provider) {
|
||||||
|
throw new Error(`Provider for ${config.resourceType} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.server.resource(
|
||||||
|
`BMAD ${config.resourceType}`,
|
||||||
|
new ResourceTemplate(`${config.uriScheme}/{${config.directory.slice(0, -1)}Id}`, {
|
||||||
|
list: async () => {
|
||||||
|
return await this.listIndividualResources(config, provider);
|
||||||
|
},
|
||||||
|
complete: {
|
||||||
|
[`${config.directory.slice(0, -1)}Id`]: async (value: string) => {
|
||||||
|
return await this.completeResourceId(value, config, provider);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
description: config.description,
|
||||||
|
},
|
||||||
|
async (uri: URL, variables: Variables) => {
|
||||||
|
return await this.readIndividualResource(uri, variables, config, provider);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async listIndividualResources(
|
||||||
|
config: ResourceConfig,
|
||||||
|
provider: ResourceProvider<BaseResource>
|
||||||
|
): Promise<ListResourcesResult> {
|
||||||
|
try {
|
||||||
|
const resources = await provider.discover();
|
||||||
|
const resourceList = resources.map((resource) => ({
|
||||||
|
uri: `${config.uriScheme}/${resource.id}`,
|
||||||
|
name: resource.name,
|
||||||
|
title: `${config.resourceType} ${resource.name.replace(config.nameSuffix, "")}`,
|
||||||
|
description:
|
||||||
|
resource.description || `BMAD ${config.resourceType.toLowerCase()}: ${resource.name}`,
|
||||||
|
mimeType: this.getMimeTypeForExtension(resource.extension),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { resources: resourceList };
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Error listing individual ${config.resourceType.toLowerCase()} resources:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
`Failed to list ${config.resourceType.toLowerCase()} resources: ${(error as Error).message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async readIndividualResource(
|
||||||
|
uri: URL,
|
||||||
|
variables: Variables,
|
||||||
|
config: ResourceConfig,
|
||||||
|
provider: ResourceProvider<BaseResource>
|
||||||
|
): Promise<ReadResourceResult> {
|
||||||
|
try {
|
||||||
|
const resourceIdKey = `${config.directory.slice(0, -1)}Id`;
|
||||||
|
const resourceIdRaw = variables[resourceIdKey];
|
||||||
|
|
||||||
|
if (!resourceIdRaw) {
|
||||||
|
throw new Error(`${config.resourceType} ID is required in URI`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the case where resourceId might be an array
|
||||||
|
const resourceId = Array.isArray(resourceIdRaw) ? resourceIdRaw[0] : resourceIdRaw;
|
||||||
|
if (!resourceId) {
|
||||||
|
throw new Error(`${config.resourceType} ID is required in URI`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get resource content
|
||||||
|
const resource = await provider.get(resourceId);
|
||||||
|
const mimeType = this.getMimeTypeForExtension(resource.extension);
|
||||||
|
|
||||||
|
return {
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri: uri.toString(),
|
||||||
|
mimeType,
|
||||||
|
text: resource.content || "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Error reading individual ${config.resourceType.toLowerCase()} resource:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
`Failed to read ${config.resourceType.toLowerCase()} resource: ${(error as Error).message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async completeResourceId(
|
||||||
|
value: string,
|
||||||
|
config: ResourceConfig,
|
||||||
|
provider: ResourceProvider<BaseResource>
|
||||||
|
): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const resources = await provider.discover();
|
||||||
|
|
||||||
|
// Filter resources that start with the provided value
|
||||||
|
const matchingIds = resources
|
||||||
|
.map((resource) => resource.id)
|
||||||
|
.filter((id) => id.toLowerCase().startsWith(value.toLowerCase()))
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
return matchingIds;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error completing ${config.resourceType.toLowerCase()} resource IDs:`, error);
|
||||||
|
// Return empty array on error rather than throwing
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getMimeTypeForExtension(extension: string): string {
|
||||||
|
const mimeTypes: { [key: string]: string } = {
|
||||||
|
md: "text/markdown",
|
||||||
|
txt: "text/plain",
|
||||||
|
json: "application/json",
|
||||||
|
yml: "application/x-yaml",
|
||||||
|
yaml: "application/x-yaml",
|
||||||
|
};
|
||||||
|
return mimeTypes[extension] || "text/plain";
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setupPrompts() {
|
||||||
|
for (const config of this.promptConfigs) {
|
||||||
|
await this.setupPrompt(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setupPrompt(config: PromptConfig) {
|
||||||
|
const provider = this.resourceProviders.get(config.referencedResourceConfig.resourceType);
|
||||||
|
|
||||||
|
if (!provider) {
|
||||||
|
throw new Error(`Provider for ${config.referencedResourceConfig.resourceType} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resourceList = await this.listIndividualResources(
|
||||||
|
config.referencedResourceConfig,
|
||||||
|
provider
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const resource of resourceList.resources) {
|
||||||
|
const resourceContent = await provider.get(resource.name);
|
||||||
|
|
||||||
|
let dependencyResources: Resource[] = [];
|
||||||
|
|
||||||
|
if (config.parseDependencies && resourceContent.content) {
|
||||||
|
const parsedDependencies = this.getDependenciesFromYamlEmbeddedInMarkdown(
|
||||||
|
resourceContent.content,
|
||||||
|
resource.name
|
||||||
|
);
|
||||||
|
|
||||||
|
dependencyResources = await this.getDependencyResources(parsedDependencies);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.server.prompt(`BMAD ${resource.name}`, async () => {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
description: `BMAD ${resource.name}`,
|
||||||
|
messages: [
|
||||||
|
// TODO also add separate prompts for each task of the agent, so they can be triggered directly - in that case it would be great to directly embed resources instead of resource_links to save requests (relevant for ai agents like copilot which deduct premium requests and not token count)
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: {
|
||||||
|
type: "text",
|
||||||
|
text: `Please use the ${resource.name} resource to assist with your task.`,
|
||||||
|
},
|
||||||
|
} as const,
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: {
|
||||||
|
type: "resource",
|
||||||
|
resource: {
|
||||||
|
uri: resource.uri,
|
||||||
|
mimeType: resource.mimeType,
|
||||||
|
text: resourceContent.content ?? "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const,
|
||||||
|
...dependencyResources.map(
|
||||||
|
(depResource) =>
|
||||||
|
({
|
||||||
|
role: "user",
|
||||||
|
content: {
|
||||||
|
type: "resource_link",
|
||||||
|
name: depResource.name,
|
||||||
|
uri: depResource.uri,
|
||||||
|
mimeType: depResource.mimeType,
|
||||||
|
},
|
||||||
|
} as const)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error loading prompt resource ${resource.id}:`, error);
|
||||||
|
throw new Error(
|
||||||
|
`Failed to load ${config.referencedResourceConfig.resourceType.toLowerCase()} prompt: ${
|
||||||
|
(error as Error).message
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDependenciesFromYamlEmbeddedInMarkdown(
|
||||||
|
content: string,
|
||||||
|
fileName: string
|
||||||
|
): Record<string, string[]> {
|
||||||
|
try {
|
||||||
|
// Look for YAML block in markdown - it should be between ```yaml and ```
|
||||||
|
const yamlMatch = content.match(/```y(a)?ml([\s\S]*)```/);
|
||||||
|
|
||||||
|
if (!yamlMatch) {
|
||||||
|
console.warn(`No YAML block found in file ${fileName}`);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const yamlContent = yamlMatch[2];
|
||||||
|
const yamlData = load(yamlContent);
|
||||||
|
const dependencies = (yamlData as any)?.dependencies || null;
|
||||||
|
return dependencies;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error parsing YAML front matter for file ${fileName}:`, error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getDependencyResources(
|
||||||
|
dependencies: Record<string, string[]>
|
||||||
|
): Promise<Resource[]> {
|
||||||
|
const dependencyResources: Resource[] = [];
|
||||||
|
|
||||||
|
for (const [directory, dependencyList] of Object.entries(dependencies)) {
|
||||||
|
// Find the matching resource config for this dependency type
|
||||||
|
const config = this.resourceConfigs.find(
|
||||||
|
(c) => c.directory.toLowerCase() === directory.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
console.warn(`No resource config found for dependency type: ${directory}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = this.resourceProviders.get(config.resourceType);
|
||||||
|
if (!provider) {
|
||||||
|
console.warn(`No provider found for resource type: ${config.resourceType}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all resources for this type
|
||||||
|
const resourceList = await this.listIndividualResources(config, provider);
|
||||||
|
|
||||||
|
// Filter resources that match the dependency names
|
||||||
|
const matchingResources = resourceList.resources.filter((resource) =>
|
||||||
|
dependencyList.some(
|
||||||
|
(dep) =>
|
||||||
|
resource.name.toLowerCase().includes(dep.toLowerCase()) ||
|
||||||
|
resource.title?.toLowerCase().includes(dep.toLowerCase())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
dependencyResources.push(...matchingResources);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencyResources;
|
||||||
|
}
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
console.error("Starting BMAD-MCP Server...");
|
||||||
|
|
||||||
|
this.setupResources();
|
||||||
|
await this.setupPrompts();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.server.connect(this.transport);
|
||||||
|
console.error("BMAD-MCP Server started successfully on stdio transport");
|
||||||
|
|
||||||
|
// Handle graceful shutdown
|
||||||
|
process.on("SIGINT", () => this.shutdown());
|
||||||
|
process.on("SIGTERM", () => this.shutdown());
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to start BMAD-MCP Server:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async shutdown() {
|
||||||
|
console.error("Shutting down BMAD-MCP Server...");
|
||||||
|
try {
|
||||||
|
await this.server.close();
|
||||||
|
console.error("BMAD-MCP Server shut down gracefully");
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during shutdown:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
// Shared types for the project
|
||||||
|
|
||||||
|
export interface ServerInfo {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerConfiguration {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerCapabilities {
|
||||||
|
// Empty for minimal implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic resource types
|
||||||
|
export interface BaseResource {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
extension: string;
|
||||||
|
content?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResourceProvider<T extends BaseResource> {
|
||||||
|
discover(): Promise<T[]>;
|
||||||
|
get(id: string): Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResourceConfig {
|
||||||
|
resourceType: string;
|
||||||
|
uriScheme: string;
|
||||||
|
directory: string;
|
||||||
|
nameSuffix: string;
|
||||||
|
description: string;
|
||||||
|
supportedExtensions: string[];
|
||||||
|
enhanceResource?: (baseResource: BaseResource) => BaseResource;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PromptConfig {
|
||||||
|
referencedResourceConfig: ResourceConfig;
|
||||||
|
parseDependencies?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
@ -0,0 +1,185 @@
|
||||||
|
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
import { ResourceProvider } from "../../src/providers/resourceProvider.js";
|
||||||
|
import type { BaseResource, ResourceConfig } from "../../src/types/index.js";
|
||||||
|
|
||||||
|
// Mock fs module
|
||||||
|
vi.mock("fs", () => ({
|
||||||
|
promises: {
|
||||||
|
readdir: vi.fn(),
|
||||||
|
readFile: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockFs = vi.mocked(fs);
|
||||||
|
|
||||||
|
interface TestResource extends BaseResource {
|
||||||
|
testProperty?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestResourceProvider extends ResourceProvider<TestResource> {
|
||||||
|
constructor(config: ResourceConfig, baseDir?: string) {
|
||||||
|
super(config, baseDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected enhanceResource(baseResource: BaseResource): TestResource {
|
||||||
|
return {
|
||||||
|
...baseResource,
|
||||||
|
testProperty: "enhanced",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("ResourceProvider", () => {
|
||||||
|
let provider: TestResourceProvider;
|
||||||
|
const mockBaseDir = "/test/project";
|
||||||
|
const resourcesDir = join(mockBaseDir, "bmad-core/test-resources");
|
||||||
|
|
||||||
|
const testConfig: ResourceConfig = {
|
||||||
|
resourceType: "TestResource",
|
||||||
|
uriScheme: "test://resources",
|
||||||
|
directory: "test-resources",
|
||||||
|
nameSuffix: "-test",
|
||||||
|
description: "Test resources",
|
||||||
|
supportedExtensions: [".md", ".txt"],
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
provider = new TestResourceProvider(testConfig, mockBaseDir);
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("discoverResources", () => {
|
||||||
|
it("should return empty array when resources directory does not exist", async () => {
|
||||||
|
mockFs.readdir.mockRejectedValue({ code: "ENOENT" });
|
||||||
|
|
||||||
|
const resources = await provider.discoverResources();
|
||||||
|
|
||||||
|
expect(resources).toEqual([]);
|
||||||
|
expect(mockFs.readdir).toHaveBeenCalledWith(resourcesDir, { withFileTypes: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should discover resource files with supported extensions", async () => {
|
||||||
|
const mockDirents = [
|
||||||
|
{ name: "resource1.md", isDirectory: () => false, isFile: () => true },
|
||||||
|
{ name: "resource2.txt", isDirectory: () => false, isFile: () => true },
|
||||||
|
{ name: "ignored.json", isDirectory: () => false, isFile: () => true }, // Should be ignored
|
||||||
|
{ name: "resource3.md", isDirectory: () => false, isFile: () => true },
|
||||||
|
];
|
||||||
|
|
||||||
|
mockFs.readdir.mockResolvedValue(mockDirents as any);
|
||||||
|
|
||||||
|
const resources = await provider.discoverResources();
|
||||||
|
|
||||||
|
expect(resources).toHaveLength(3);
|
||||||
|
expect(resources[0]).toEqual({
|
||||||
|
id: "resource1",
|
||||||
|
name: "resource1",
|
||||||
|
path: join(resourcesDir, "resource1.md"),
|
||||||
|
extension: "md",
|
||||||
|
testProperty: "enhanced",
|
||||||
|
});
|
||||||
|
expect(resources[1]).toEqual({
|
||||||
|
id: "resource2",
|
||||||
|
name: "resource2",
|
||||||
|
path: join(resourcesDir, "resource2.txt"),
|
||||||
|
extension: "txt",
|
||||||
|
testProperty: "enhanced",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getResource", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const mockDirents = [
|
||||||
|
{ name: "test-resource.md", isDirectory: () => false, isFile: () => true },
|
||||||
|
];
|
||||||
|
mockFs.readdir.mockResolvedValue(mockDirents as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should load resource content", async () => {
|
||||||
|
const mockContent = "# Test Resource\n\nThis is test content.";
|
||||||
|
mockFs.readFile.mockResolvedValue(mockContent);
|
||||||
|
|
||||||
|
const resource = await provider.getResource("test-resource");
|
||||||
|
|
||||||
|
expect(resource.name).toBe("test-resource");
|
||||||
|
expect(resource.content).toBe(mockContent);
|
||||||
|
expect(resource.testProperty).toBe("enhanced");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should cache loaded resources", async () => {
|
||||||
|
mockFs.readFile.mockResolvedValue("Content");
|
||||||
|
|
||||||
|
// First call
|
||||||
|
await provider.getResource("test-resource");
|
||||||
|
// Second call
|
||||||
|
await provider.getResource("test-resource");
|
||||||
|
|
||||||
|
expect(mockFs.readFile).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getAllResources", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const mockDirents = [
|
||||||
|
{ name: "resource1.md", isDirectory: () => false, isFile: () => true },
|
||||||
|
{ name: "resource2.md", isDirectory: () => false, isFile: () => true },
|
||||||
|
];
|
||||||
|
mockFs.readdir.mockResolvedValue(mockDirents as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return all resources with content loaded", async () => {
|
||||||
|
mockFs.readFile.mockResolvedValueOnce("Content 1").mockResolvedValueOnce("Content 2");
|
||||||
|
|
||||||
|
const resources = await provider.getAllResources();
|
||||||
|
|
||||||
|
expect(resources).toHaveLength(2);
|
||||||
|
expect(resources[0].content).toBe("Content 1");
|
||||||
|
expect(resources[1].content).toBe("Content 2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should include resources without content when file read fails", async () => {
|
||||||
|
mockFs.readFile
|
||||||
|
.mockResolvedValueOnce("Content 1")
|
||||||
|
.mockRejectedValueOnce(new Error("File read error"));
|
||||||
|
|
||||||
|
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
|
||||||
|
const resources = await provider.getAllResources();
|
||||||
|
|
||||||
|
expect(resources).toHaveLength(2);
|
||||||
|
expect(resources[0].content).toBe("Content 1");
|
||||||
|
expect(resources[1].content).toBeUndefined();
|
||||||
|
expect(consoleSpy).toHaveBeenCalledWith(
|
||||||
|
"Failed to load resource resource2:",
|
||||||
|
expect.any(Error)
|
||||||
|
);
|
||||||
|
|
||||||
|
consoleSpy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("supported extensions", () => {
|
||||||
|
it("should only discover files with configured extensions", async () => {
|
||||||
|
const mockDirents = [
|
||||||
|
{ name: "file.md", isDirectory: () => false, isFile: () => true },
|
||||||
|
{ name: "file.txt", isDirectory: () => false, isFile: () => true },
|
||||||
|
{ name: "file.json", isDirectory: () => false, isFile: () => true }, // Not in configured extensions
|
||||||
|
{ name: "file.doc", isDirectory: () => false, isFile: () => true }, // Not supported
|
||||||
|
];
|
||||||
|
|
||||||
|
mockFs.readdir.mockResolvedValue(mockDirents as any);
|
||||||
|
|
||||||
|
const resources = await provider.discoverResources();
|
||||||
|
|
||||||
|
expect(resources).toHaveLength(2);
|
||||||
|
expect(resources.map((r) => r.extension)).toEqual(["md", "txt"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
import { BmadMcpServer } from "../src/server.js";
|
||||||
|
import { ResourceProvider } from "../src/providers/resourceProvider.js";
|
||||||
|
import type { BaseResource } from "../src/types/index.js";
|
||||||
|
|
||||||
|
describe("BmadMcpServer - Generic Resources Integration", () => {
|
||||||
|
let server: BmadMcpServer;
|
||||||
|
let tempDir: string;
|
||||||
|
let checklistsDir: string;
|
||||||
|
let templatesDir: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Create temporary directory structure
|
||||||
|
tempDir = await fs.mkdtemp("/tmp/bmad-test-");
|
||||||
|
checklistsDir = join(tempDir, "bmad-core", "checklists");
|
||||||
|
templatesDir = join(tempDir, "bmad-core", "templates");
|
||||||
|
await fs.mkdir(checklistsDir, { recursive: true });
|
||||||
|
await fs.mkdir(templatesDir, { recursive: true });
|
||||||
|
|
||||||
|
// Create test checklist files
|
||||||
|
await fs.writeFile(
|
||||||
|
join(checklistsDir, "pm-checklist.md"),
|
||||||
|
`# PM Checklist
|
||||||
|
|
||||||
|
Product Manager requirements checklist for ensuring comprehensive project definition.
|
||||||
|
|
||||||
|
- [ ] Define user stories
|
||||||
|
- [ ] Create acceptance criteria
|
||||||
|
- [ ] Validate business requirements`
|
||||||
|
);
|
||||||
|
|
||||||
|
await fs.writeFile(
|
||||||
|
join(checklistsDir, "story-dod-checklist.md"),
|
||||||
|
`# Story Definition of Done
|
||||||
|
|
||||||
|
Checklist to ensure stories meet quality standards before marking as complete.
|
||||||
|
|
||||||
|
- [ ] All acceptance criteria met
|
||||||
|
- [ ] Code reviewed
|
||||||
|
- [ ] Tests written and passing`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create test template files
|
||||||
|
await fs.writeFile(
|
||||||
|
join(templatesDir, "prd-tmpl.md"),
|
||||||
|
`# Product Requirements Document Template
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
[Brief description of the product]
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
[Detailed requirements here]`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create category subdirectory for checklists
|
||||||
|
const devDir = join(checklistsDir, "dev");
|
||||||
|
await fs.mkdir(devDir, { recursive: true });
|
||||||
|
await fs.writeFile(
|
||||||
|
join(devDir, "code-review.md"),
|
||||||
|
`# Code Review Checklist
|
||||||
|
|
||||||
|
Developer checklist for thorough code reviews.
|
||||||
|
|
||||||
|
- [ ] Code follows standards
|
||||||
|
- [ ] Security considerations reviewed
|
||||||
|
- [ ] Performance implications considered`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize server with temp directory
|
||||||
|
server = new BmadMcpServer();
|
||||||
|
const { ResourceProvider } = await import("../src/providers/resourceProvider.js");
|
||||||
|
|
||||||
|
// Create configurations for testing
|
||||||
|
const checklistConfig = {
|
||||||
|
resourceType: "Checklist",
|
||||||
|
uriScheme: "bmad://checklists",
|
||||||
|
directory: "checklists",
|
||||||
|
nameSuffix: "-checklist",
|
||||||
|
description: "Individual BMAD checklist content",
|
||||||
|
supportedExtensions: [".md"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const templateConfig = {
|
||||||
|
resourceType: "Template",
|
||||||
|
uriScheme: "bmad://templates",
|
||||||
|
directory: "templates",
|
||||||
|
nameSuffix: "-tmpl",
|
||||||
|
description: "Individual BMAD template content",
|
||||||
|
supportedExtensions: [".md"],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Override the resource providers to use our test directory
|
||||||
|
(server as any).resourceProviders.set(
|
||||||
|
"Checklist",
|
||||||
|
new ResourceProvider(checklistConfig, tempDir)
|
||||||
|
);
|
||||||
|
(server as any).resourceProviders.set(
|
||||||
|
"Template",
|
||||||
|
new ResourceProvider(templateConfig, tempDir)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
// Clean up temp directory
|
||||||
|
await fs.rm(tempDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Checklist Resources", () => {
|
||||||
|
it("should discover and list all checklist resources", async () => {
|
||||||
|
const checklistConfig = (server as any).resourceConfigs.find(
|
||||||
|
(config: any) => config.resourceType === "Checklist"
|
||||||
|
);
|
||||||
|
const provider = (server as any).resourceProviders.get("Checklist");
|
||||||
|
|
||||||
|
const result = await (server as any).listIndividualResources(checklistConfig, provider);
|
||||||
|
|
||||||
|
expect(result.resources).toHaveLength(3);
|
||||||
|
|
||||||
|
const checklistUris = result.resources.map((r: any) => r.uri);
|
||||||
|
expect(checklistUris).toContain("bmad://checklists/pm-checklist");
|
||||||
|
expect(checklistUris).toContain("bmad://checklists/story-dod-checklist");
|
||||||
|
expect(checklistUris).toContain("bmad://checklists/dev_code-review");
|
||||||
|
|
||||||
|
// Verify metadata
|
||||||
|
const pmChecklist = result.resources.find(
|
||||||
|
(r: any) => r.uri === "bmad://checklists/pm-checklist"
|
||||||
|
);
|
||||||
|
expect(pmChecklist).toMatchObject({
|
||||||
|
name: "pm-checklist",
|
||||||
|
title: "Checklist pm",
|
||||||
|
description: "BMAD checklist: pm-checklist",
|
||||||
|
mimeType: "text/markdown",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should retrieve individual checklist content", async () => {
|
||||||
|
const uri = new URL("bmad://checklists/pm-checklist");
|
||||||
|
const variables = { checklistId: "pm-checklist" };
|
||||||
|
const checklistConfig = (server as any).resourceConfigs.find(
|
||||||
|
(config: any) => config.resourceType === "Checklist"
|
||||||
|
);
|
||||||
|
const provider = (server as any).resourceProviders.get("Checklist");
|
||||||
|
|
||||||
|
const result = await (server as any).readIndividualResource(
|
||||||
|
uri,
|
||||||
|
variables,
|
||||||
|
checklistConfig,
|
||||||
|
provider
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.contents).toHaveLength(1);
|
||||||
|
expect(result.contents[0]).toMatchObject({
|
||||||
|
uri: "bmad://checklists/pm-checklist",
|
||||||
|
mimeType: "text/markdown",
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = result.contents[0].text;
|
||||||
|
expect(content).toContain("# PM Checklist");
|
||||||
|
expect(content).toContain("Product Manager requirements checklist");
|
||||||
|
expect(content).toContain("- [ ] Define user stories");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Template Resources", () => {
|
||||||
|
it("should discover and list all template resources", async () => {
|
||||||
|
const templateConfig = (server as any).resourceConfigs.find(
|
||||||
|
(config: any) => config.resourceType === "Template"
|
||||||
|
);
|
||||||
|
const provider = (server as any).resourceProviders.get("Template");
|
||||||
|
|
||||||
|
const result = await (server as any).listIndividualResources(templateConfig, provider);
|
||||||
|
|
||||||
|
expect(result.resources).toHaveLength(1);
|
||||||
|
|
||||||
|
const templateUris = result.resources.map((r: any) => r.uri);
|
||||||
|
expect(templateUris).toContain("bmad://templates/prd-tmpl");
|
||||||
|
|
||||||
|
// Verify metadata
|
||||||
|
const prdTemplate = result.resources.find((r: any) => r.uri === "bmad://templates/prd-tmpl");
|
||||||
|
expect(prdTemplate).toMatchObject({
|
||||||
|
name: "prd-tmpl",
|
||||||
|
title: "Template prd",
|
||||||
|
description: "BMAD template: prd-tmpl",
|
||||||
|
mimeType: "text/markdown",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should retrieve individual template content", async () => {
|
||||||
|
const uri = new URL("bmad://templates/prd-tmpl");
|
||||||
|
const variables = { templateId: "prd-tmpl" };
|
||||||
|
const templateConfig = (server as any).resourceConfigs.find(
|
||||||
|
(config: any) => config.resourceType === "Template"
|
||||||
|
);
|
||||||
|
const provider = (server as any).resourceProviders.get("Template");
|
||||||
|
|
||||||
|
const result = await (server as any).readIndividualResource(
|
||||||
|
uri,
|
||||||
|
variables,
|
||||||
|
templateConfig,
|
||||||
|
provider
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.contents).toHaveLength(1);
|
||||||
|
expect(result.contents[0]).toMatchObject({
|
||||||
|
uri: "bmad://templates/prd-tmpl",
|
||||||
|
mimeType: "text/markdown",
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = result.contents[0].text;
|
||||||
|
expect(content).toContain("# Product Requirements Document Template");
|
||||||
|
expect(content).toContain("## Overview");
|
||||||
|
expect(content).toContain("## Requirements");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,350 @@
|
||||||
|
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||||
|
import { BmadMcpServer } from "../src/server.js";
|
||||||
|
import { ResourceProvider } from "../src/providers/resourceProvider.js";
|
||||||
|
import type { BaseResource } from "../src/types/index.js";
|
||||||
|
|
||||||
|
// Mock the stdio transport to avoid actual process interaction during tests
|
||||||
|
vi.mock("@modelcontextprotocol/sdk/server/stdio.js", () => ({
|
||||||
|
StdioServerTransport: vi.fn().mockImplementation(() => ({
|
||||||
|
start: vi.fn(),
|
||||||
|
on: vi.fn(),
|
||||||
|
close: vi.fn(),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock the MCP server with prompt functionality
|
||||||
|
const mockPromptFn = vi.fn();
|
||||||
|
const mockServerInstance = {
|
||||||
|
connect: vi.fn().mockResolvedValue(undefined),
|
||||||
|
close: vi.fn().mockResolvedValue(undefined),
|
||||||
|
resource: vi.fn(),
|
||||||
|
prompt: mockPromptFn,
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mock("@modelcontextprotocol/sdk/server/mcp.js", () => ({
|
||||||
|
McpServer: vi.fn().mockImplementation(() => mockServerInstance),
|
||||||
|
ResourceTemplate: vi.fn().mockImplementation((pattern, callbacks) => ({
|
||||||
|
uriTemplate: pattern,
|
||||||
|
listCallback: callbacks.list,
|
||||||
|
completeCallback: callbacks.complete,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock the resource provider
|
||||||
|
vi.mock("../src/providers/resourceProvider.js", () => ({
|
||||||
|
ResourceProvider: vi.fn().mockImplementation(() => ({
|
||||||
|
discover: vi.fn(),
|
||||||
|
get: vi.fn(),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockResourceProvider = vi.mocked(ResourceProvider);
|
||||||
|
|
||||||
|
describe("BmadMcpServer - MCP Prompts", () => {
|
||||||
|
let server: BmadMcpServer;
|
||||||
|
let mockProviderInstance: any;
|
||||||
|
let consoleErrorSpy: any;
|
||||||
|
|
||||||
|
const mockAgents: BaseResource[] = [
|
||||||
|
{
|
||||||
|
id: "bmad-orchestrator",
|
||||||
|
name: "BMAD Orchestrator",
|
||||||
|
path: "/test/.bmad-core/agents/bmad-orchestrator.md",
|
||||||
|
extension: "md",
|
||||||
|
content: "# BMAD Orchestrator\nThis is the main orchestrator agent.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "prd-agent",
|
||||||
|
name: "PRD Agent",
|
||||||
|
path: "/test/.bmad-core/agents/prd-agent.md",
|
||||||
|
extension: "md",
|
||||||
|
content: "# PRD Agent\nThis agent helps create product requirements documents.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "story-agent",
|
||||||
|
name: "Story Agent",
|
||||||
|
path: "/test/.bmad-core/agents/story-agent.md",
|
||||||
|
extension: "md",
|
||||||
|
content: "# Story Agent\nThis agent helps create user stories.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Create mock provider instance
|
||||||
|
mockProviderInstance = {
|
||||||
|
discover: vi.fn(),
|
||||||
|
get: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
mockResourceProvider.mockImplementation(() => mockProviderInstance);
|
||||||
|
|
||||||
|
server = new BmadMcpServer();
|
||||||
|
|
||||||
|
// Override the server instance with our mock after construction
|
||||||
|
(server as any).server = mockServerInstance;
|
||||||
|
|
||||||
|
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
consoleErrorSpy.mockRestore();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Prompt Setup", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockProviderInstance.discover.mockResolvedValue(mockAgents);
|
||||||
|
mockAgents.forEach((agent) => {
|
||||||
|
mockProviderInstance.get.mockImplementation((name: string) => {
|
||||||
|
const found = mockAgents.find((a) => a.name === name);
|
||||||
|
return Promise.resolve(found || null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should setup prompts during server start", async () => {
|
||||||
|
// Mock the server methods to avoid actual connection
|
||||||
|
vi.spyOn(server as any, "setupResources").mockImplementation(() => {});
|
||||||
|
mockServerInstance.connect = vi.fn().mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await server.start();
|
||||||
|
|
||||||
|
// Verify that prompts were registered for each agent
|
||||||
|
expect(mockPromptFn).toHaveBeenCalledTimes(mockAgents.length);
|
||||||
|
|
||||||
|
// Verify prompt names
|
||||||
|
mockAgents.forEach((agent) => {
|
||||||
|
expect(mockPromptFn).toHaveBeenCalledWith(`BMAD ${agent.name}`, expect.any(Function));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create proper prompt structure with resource content", async () => {
|
||||||
|
// Access the setupPrompt method directly for testing
|
||||||
|
const setupPrompt = (server as any).setupPrompt.bind(server);
|
||||||
|
const agentConfig = (server as any).agentsResourceConfig;
|
||||||
|
|
||||||
|
await setupPrompt({ referencedResourceConfig: agentConfig });
|
||||||
|
|
||||||
|
// Verify prompt was called with correct structure
|
||||||
|
expect(mockPromptFn).toHaveBeenCalledTimes(mockAgents.length);
|
||||||
|
|
||||||
|
// Get the prompt handler function for the first agent
|
||||||
|
const promptCalls = mockPromptFn.mock.calls;
|
||||||
|
const firstPromptHandler = promptCalls[0][1];
|
||||||
|
|
||||||
|
// Execute the prompt handler to test its structure
|
||||||
|
const promptResult = await firstPromptHandler();
|
||||||
|
|
||||||
|
expect(promptResult).toEqual({
|
||||||
|
description: `BMAD ${mockAgents[0].name}`,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: {
|
||||||
|
type: "text",
|
||||||
|
text: `Please use the ${mockAgents[0].name} resource to assist with your task.`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: {
|
||||||
|
type: "resource",
|
||||||
|
resource: {
|
||||||
|
uri: `bmad://agents/${mockAgents[0].id}`,
|
||||||
|
mimeType: "text/markdown",
|
||||||
|
text: mockAgents[0].content,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle empty content in resources", async () => {
|
||||||
|
const agentWithNoContent = {
|
||||||
|
...mockAgents[0],
|
||||||
|
content: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
mockProviderInstance.discover.mockResolvedValue([agentWithNoContent]);
|
||||||
|
mockProviderInstance.get.mockResolvedValue(agentWithNoContent);
|
||||||
|
|
||||||
|
const setupPrompt = (server as any).setupPrompt.bind(server);
|
||||||
|
const agentConfig = (server as any).agentsResourceConfig;
|
||||||
|
|
||||||
|
await setupPrompt({ referencedResourceConfig: agentConfig });
|
||||||
|
|
||||||
|
const promptCalls = mockPromptFn.mock.calls;
|
||||||
|
const promptHandler = promptCalls[0][1];
|
||||||
|
const promptResult = await promptHandler();
|
||||||
|
|
||||||
|
expect(promptResult.messages[1].content.resource.text).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use correct MIME types for different extensions", async () => {
|
||||||
|
const yamlAgent = {
|
||||||
|
...mockAgents[0],
|
||||||
|
extension: "yml",
|
||||||
|
};
|
||||||
|
|
||||||
|
mockProviderInstance.discover.mockResolvedValue([yamlAgent]);
|
||||||
|
mockProviderInstance.get.mockResolvedValue(yamlAgent);
|
||||||
|
|
||||||
|
const setupPrompt = (server as any).setupPrompt.bind(server);
|
||||||
|
const agentConfig = (server as any).agentsResourceConfig;
|
||||||
|
|
||||||
|
await setupPrompt({ referencedResourceConfig: agentConfig });
|
||||||
|
|
||||||
|
const promptCalls = mockPromptFn.mock.calls;
|
||||||
|
const promptHandler = promptCalls[0][1];
|
||||||
|
const promptResult = await promptHandler();
|
||||||
|
|
||||||
|
expect(promptResult.messages[1].content.resource.mimeType).toBe("application/x-yaml");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Error Handling", () => {
|
||||||
|
it("should throw error when provider is not found", async () => {
|
||||||
|
const invalidConfig = {
|
||||||
|
referencedResourceConfig: {
|
||||||
|
resourceType: "NonExistentType",
|
||||||
|
uriScheme: "bmad://invalid",
|
||||||
|
directory: "invalid",
|
||||||
|
nameSuffix: "",
|
||||||
|
description: "Invalid type",
|
||||||
|
supportedExtensions: [".md"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupPrompt = (server as any).setupPrompt.bind(server);
|
||||||
|
|
||||||
|
await expect(setupPrompt(invalidConfig)).rejects.toThrow(
|
||||||
|
"Provider for NonExistentType not found"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle resource discovery errors", async () => {
|
||||||
|
mockProviderInstance.discover.mockRejectedValue(new Error("Discovery failed"));
|
||||||
|
|
||||||
|
const setupPrompt = (server as any).setupPrompt.bind(server);
|
||||||
|
const agentConfig = (server as any).agentsResourceConfig;
|
||||||
|
|
||||||
|
await expect(setupPrompt({ referencedResourceConfig: agentConfig })).rejects.toThrow(
|
||||||
|
"Failed to list agent resources: Discovery failed"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle resource loading errors in prompt execution", async () => {
|
||||||
|
// This test should actually test setup errors, since resource loading happens during setup
|
||||||
|
mockProviderInstance.discover.mockResolvedValue(mockAgents);
|
||||||
|
mockProviderInstance.get.mockRejectedValue(new Error("Resource not found"));
|
||||||
|
|
||||||
|
const setupPrompt = (server as any).setupPrompt.bind(server);
|
||||||
|
const agentConfig = (server as any).agentsResourceConfig;
|
||||||
|
|
||||||
|
// The error should happen during setup, not prompt execution
|
||||||
|
await expect(setupPrompt({ referencedResourceConfig: agentConfig })).rejects.toThrow(
|
||||||
|
"Resource not found"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle errors in prompt execution gracefully", async () => {
|
||||||
|
const agentWithBadContent = {
|
||||||
|
...mockAgents[0],
|
||||||
|
id: "bad-agent",
|
||||||
|
content: "# Bad Agent\nThis agent has content.",
|
||||||
|
};
|
||||||
|
|
||||||
|
mockProviderInstance.discover.mockResolvedValue([agentWithBadContent]);
|
||||||
|
mockProviderInstance.get.mockResolvedValue(agentWithBadContent);
|
||||||
|
|
||||||
|
const setupPrompt = (server as any).setupPrompt.bind(server);
|
||||||
|
const agentConfig = (server as any).agentsResourceConfig;
|
||||||
|
|
||||||
|
await setupPrompt({ referencedResourceConfig: agentConfig });
|
||||||
|
|
||||||
|
expect(mockPromptFn).toHaveBeenCalledWith(
|
||||||
|
`BMAD ${agentWithBadContent.name}`,
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get the registered prompt handler and verify it works
|
||||||
|
const promptCalls = mockPromptFn.mock.calls;
|
||||||
|
const promptHandler = promptCalls[0][1];
|
||||||
|
|
||||||
|
const result = await promptHandler();
|
||||||
|
expect(result.description).toBe(`BMAD ${agentWithBadContent.name}`);
|
||||||
|
expect(result.messages[1].content.resource.text).toBe(agentWithBadContent.content);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Prompt Configuration", () => {
|
||||||
|
it("should have correct prompt configurations defined", () => {
|
||||||
|
const promptConfigs = (server as any).promptConfigs;
|
||||||
|
|
||||||
|
expect(promptConfigs).toHaveLength(1);
|
||||||
|
expect(promptConfigs[0].referencedResourceConfig.resourceType).toBe("Agent");
|
||||||
|
expect(promptConfigs[0].referencedResourceConfig.uriScheme).toBe("bmad://agents");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should setup prompts for all configured prompt types", async () => {
|
||||||
|
mockProviderInstance.discover.mockResolvedValue(mockAgents);
|
||||||
|
mockAgents.forEach((agent) => {
|
||||||
|
mockProviderInstance.get.mockImplementation((name: string) => {
|
||||||
|
const found = mockAgents.find((a) => a.name === name);
|
||||||
|
return Promise.resolve(found || null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const setupPrompts = (server as any).setupPrompts.bind(server);
|
||||||
|
await setupPrompts();
|
||||||
|
|
||||||
|
// Should call setupPrompt once for each prompt config
|
||||||
|
expect(mockPromptFn).toHaveBeenCalledTimes(mockAgents.length);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Resource Integration", () => {
|
||||||
|
it("should correctly map resource properties to prompt content", async () => {
|
||||||
|
const testAgent = mockAgents[0];
|
||||||
|
mockProviderInstance.discover.mockResolvedValue([testAgent]);
|
||||||
|
mockProviderInstance.get.mockResolvedValue(testAgent);
|
||||||
|
|
||||||
|
const setupPrompt = (server as any).setupPrompt.bind(server);
|
||||||
|
const agentConfig = (server as any).agentsResourceConfig;
|
||||||
|
|
||||||
|
await setupPrompt({ referencedResourceConfig: agentConfig });
|
||||||
|
|
||||||
|
const promptCalls = mockPromptFn.mock.calls;
|
||||||
|
const promptHandler = promptCalls[0][1];
|
||||||
|
const promptResult = await promptHandler();
|
||||||
|
|
||||||
|
expect(promptResult.messages[1].content.resource).toEqual({
|
||||||
|
uri: "bmad://agents/bmad-orchestrator",
|
||||||
|
mimeType: "text/markdown",
|
||||||
|
text: testAgent.content,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle multiple agents correctly", async () => {
|
||||||
|
mockProviderInstance.discover.mockResolvedValue(mockAgents);
|
||||||
|
mockAgents.forEach((agent) => {
|
||||||
|
mockProviderInstance.get.mockImplementation((name: string) => {
|
||||||
|
const found = mockAgents.find((a) => a.name === name);
|
||||||
|
return Promise.resolve(found || null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const setupPrompt = (server as any).setupPrompt.bind(server);
|
||||||
|
const agentConfig = (server as any).agentsResourceConfig;
|
||||||
|
|
||||||
|
await setupPrompt({ referencedResourceConfig: agentConfig });
|
||||||
|
|
||||||
|
expect(mockPromptFn).toHaveBeenCalledTimes(mockAgents.length);
|
||||||
|
|
||||||
|
// Verify each agent got its own prompt
|
||||||
|
const promptNames = mockPromptFn.mock.calls.map((call) => call[0]);
|
||||||
|
expect(promptNames).toEqual(["BMAD BMAD Orchestrator", "BMAD PRD Agent", "BMAD Story Agent"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||||
|
import { BmadMcpServer } from "../src/server.js";
|
||||||
|
import { ResourceProvider } from "../src/providers/resourceProvider.js";
|
||||||
|
import type { BaseResource } from "../src/types/index.js";
|
||||||
|
|
||||||
|
// Mock the stdio transport to avoid actual process interaction during tests
|
||||||
|
vi.mock("@modelcontextprotocol/sdk/server/stdio.js", () => ({
|
||||||
|
StdioServerTransport: vi.fn().mockImplementation(() => ({
|
||||||
|
start: vi.fn(),
|
||||||
|
on: vi.fn(),
|
||||||
|
close: vi.fn(),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock the unified resource provider
|
||||||
|
vi.mock("../src/providers/resourceProvider.js", () => ({
|
||||||
|
ResourceProvider: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockResourceProvider = vi.mocked(ResourceProvider);
|
||||||
|
|
||||||
|
describe("BmadMcpServer - Unified Resources", () => {
|
||||||
|
let server: BmadMcpServer;
|
||||||
|
let consoleErrorSpy: any;
|
||||||
|
|
||||||
|
const mockResources: BaseResource[] = [
|
||||||
|
{
|
||||||
|
id: "prd-tmpl",
|
||||||
|
name: "PRD Template",
|
||||||
|
path: "/test/bmad-core/templates/prd-tmpl.md",
|
||||||
|
extension: "md",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "architecture-tmpl",
|
||||||
|
name: "Architecture Template",
|
||||||
|
path: "/test/bmad-core/templates/architecture-tmpl.md",
|
||||||
|
extension: "md",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
|
||||||
|
// Mock the constructor to return a basic mock
|
||||||
|
mockResourceProvider.mockImplementation(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
discover: vi.fn().mockResolvedValue(mockResources),
|
||||||
|
get: vi.fn().mockResolvedValue(mockResources[0]),
|
||||||
|
} as any)
|
||||||
|
);
|
||||||
|
|
||||||
|
server = new BmadMcpServer();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
consoleErrorSpy.mockRestore();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Server Construction", () => {
|
||||||
|
it("should initialize server with unified resource providers", () => {
|
||||||
|
expect(server).toBeDefined();
|
||||||
|
expect(server).toBeInstanceOf(BmadMcpServer);
|
||||||
|
// Verify ResourceProvider was called for each resource type (Agent, Checklist, Data, Task, Template, Util, Workflow)
|
||||||
|
expect(mockResourceProvider).toHaveBeenCalledTimes(7);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("MIME Type Detection", () => {
|
||||||
|
it("should return correct MIME types for different extensions", () => {
|
||||||
|
const mimeTypeMethod = (server as any).getMimeTypeForExtension.bind(server);
|
||||||
|
|
||||||
|
expect(mimeTypeMethod("md")).toBe("text/markdown");
|
||||||
|
expect(mimeTypeMethod("txt")).toBe("text/plain");
|
||||||
|
expect(mimeTypeMethod("json")).toBe("application/json");
|
||||||
|
expect(mimeTypeMethod("unknown")).toBe("text/plain");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,168 @@
|
||||||
|
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||||
|
import { BmadMcpServer } from "../src/server.js";
|
||||||
|
import { ResourceProvider } from "../src/providers/resourceProvider.js";
|
||||||
|
import type { BaseResource } from "../src/types/index.js";
|
||||||
|
|
||||||
|
// Mock the stdio transport to avoid actual process interaction during tests
|
||||||
|
vi.mock("@modelcontextprotocol/sdk/server/stdio.js", () => ({
|
||||||
|
StdioServerTransport: vi.fn().mockImplementation(() => ({
|
||||||
|
start: vi.fn(),
|
||||||
|
on: vi.fn(),
|
||||||
|
close: vi.fn(),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock the unified resource provider
|
||||||
|
vi.mock("../src/providers/resourceProvider.js", () => ({
|
||||||
|
ResourceProvider: vi.fn().mockImplementation(() => ({
|
||||||
|
discover: vi.fn(),
|
||||||
|
get: vi.fn(),
|
||||||
|
discoverResources: vi.fn(),
|
||||||
|
getResource: vi.fn(),
|
||||||
|
getAllResources: vi.fn(),
|
||||||
|
clearCache: vi.fn(),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockResourceProvider = vi.mocked(ResourceProvider);
|
||||||
|
|
||||||
|
describe("BmadMcpServer - MCP Resources", () => {
|
||||||
|
let server: BmadMcpServer;
|
||||||
|
let mockProviderInstance: any;
|
||||||
|
let consoleErrorSpy: any;
|
||||||
|
|
||||||
|
const mockTemplates: BaseResource[] = [
|
||||||
|
{
|
||||||
|
id: "prd-tmpl",
|
||||||
|
name: "PRD Template",
|
||||||
|
path: "/test/bmad-core/templates/prd-tmpl.md",
|
||||||
|
extension: "md",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "architecture-tmpl",
|
||||||
|
name: "Architecture Template",
|
||||||
|
path: "/test/bmad-core/templates/architecture-tmpl.md",
|
||||||
|
extension: "md",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "story-tmpl",
|
||||||
|
name: "Story Template",
|
||||||
|
path: "/test/bmad-core/templates/story-tmpl.txt",
|
||||||
|
extension: "txt",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Create mock provider instance
|
||||||
|
mockProviderInstance = {
|
||||||
|
discover: vi.fn(),
|
||||||
|
get: vi.fn(),
|
||||||
|
discoverResources: vi.fn(),
|
||||||
|
getResource: vi.fn(),
|
||||||
|
getAllResources: vi.fn(),
|
||||||
|
clearCache: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
mockResourceProvider.mockImplementation(() => mockProviderInstance);
|
||||||
|
|
||||||
|
server = new BmadMcpServer();
|
||||||
|
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
consoleErrorSpy.mockRestore();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Template Resources Registration", () => {
|
||||||
|
it("should register resources during server construction", () => {
|
||||||
|
// Verify server was created (constructor ran successfully)
|
||||||
|
expect(server).toBeDefined();
|
||||||
|
expect(server).toBeInstanceOf(BmadMcpServer);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have template provider initialized", () => {
|
||||||
|
expect(mockResourceProvider).toHaveBeenCalledTimes(7); // Agent, Checklist, Data, Task, Template, Util, Workflow
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Individual Template Resources", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockProviderInstance.discover.mockResolvedValue(mockTemplates);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should list individual template resources", async () => {
|
||||||
|
// Test the provider directly since the server methods are now private
|
||||||
|
const templates = await (server as any).resourceProviders.get("Template").discover();
|
||||||
|
|
||||||
|
expect(templates).toEqual(mockTemplates);
|
||||||
|
expect(mockProviderInstance.discover).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should read individual template content", async () => {
|
||||||
|
const mockTemplateWithContent = {
|
||||||
|
...mockTemplates[0],
|
||||||
|
content: "# PRD Template\nThis is a product requirements document template.",
|
||||||
|
};
|
||||||
|
|
||||||
|
mockProviderInstance.get.mockResolvedValue(mockTemplateWithContent);
|
||||||
|
|
||||||
|
// Test the provider directly
|
||||||
|
const result = await (server as any).resourceProviders.get("Template").get("prd-tmpl");
|
||||||
|
|
||||||
|
expect(result).toEqual(mockTemplateWithContent);
|
||||||
|
expect(mockProviderInstance.get).toHaveBeenCalledWith("prd-tmpl");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle array templateId variables", async () => {
|
||||||
|
const mockTemplateWithContent = {
|
||||||
|
...mockTemplates[0],
|
||||||
|
content: "# PRD Template\nThis is a product requirements document template.",
|
||||||
|
};
|
||||||
|
|
||||||
|
mockProviderInstance.get.mockResolvedValue(mockTemplateWithContent);
|
||||||
|
|
||||||
|
// Test that provider handles single ID correctly
|
||||||
|
const result = await (server as any).resourceProviders.get("Template").get("prd-tmpl");
|
||||||
|
|
||||||
|
expect(result).toEqual(mockTemplateWithContent);
|
||||||
|
expect(mockProviderInstance.get).toHaveBeenCalledWith("prd-tmpl");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle missing templateId", async () => {
|
||||||
|
// Test provider error handling for missing ID
|
||||||
|
mockProviderInstance.get.mockRejectedValue(new Error("Template with ID '' not found"));
|
||||||
|
|
||||||
|
await expect((server as any).resourceProviders.get("Template").get("")).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle template loading errors", async () => {
|
||||||
|
mockProviderInstance.get.mockRejectedValue(new Error("Template not found"));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
(server as any).resourceProviders.get("Template").get("missing")
|
||||||
|
).rejects.toThrow("Template not found");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("MIME Type Detection", () => {
|
||||||
|
it("should return correct MIME types for different extensions", () => {
|
||||||
|
const getMimeType = server.getMimeTypeForExtension.bind(server);
|
||||||
|
|
||||||
|
expect(getMimeType("md")).toBe("text/markdown");
|
||||||
|
expect(getMimeType("txt")).toBe("text/plain");
|
||||||
|
expect(getMimeType("json")).toBe("application/json");
|
||||||
|
expect(getMimeType("unknown")).toBe("text/plain");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Error Handling", () => {
|
||||||
|
it("should handle template discovery errors in listing", async () => {
|
||||||
|
mockProviderInstance.discover.mockRejectedValue(new Error("Discovery failed"));
|
||||||
|
|
||||||
|
await expect((server as any).resourceProviders.get("Template").discover()).rejects.toThrow(
|
||||||
|
"Discovery failed"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
import { BmadMcpServer } from "../src/server.js";
|
||||||
|
import { ResourceProvider } from "../src/providers/resourceProvider.js";
|
||||||
|
|
||||||
|
describe("BmadMcpServer - Unified Resources Integration", () => {
|
||||||
|
let server: BmadMcpServer;
|
||||||
|
let tempDir: string;
|
||||||
|
let checklistsDir: string;
|
||||||
|
let templatesDir: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Create temporary directory structure
|
||||||
|
tempDir = await fs.mkdtemp("/tmp/bmad-test-");
|
||||||
|
checklistsDir = join(tempDir, "bmad-core", "checklists");
|
||||||
|
templatesDir = join(tempDir, "bmad-core", "templates");
|
||||||
|
await fs.mkdir(checklistsDir, { recursive: true });
|
||||||
|
await fs.mkdir(templatesDir, { recursive: true });
|
||||||
|
|
||||||
|
// Create test checklist files
|
||||||
|
await fs.writeFile(
|
||||||
|
join(checklistsDir, "pm-checklist.md"),
|
||||||
|
`# PM Checklist
|
||||||
|
|
||||||
|
Product Manager requirements checklist for ensuring comprehensive project definition.
|
||||||
|
|
||||||
|
- [ ] Define user stories
|
||||||
|
- [ ] Create acceptance criteria
|
||||||
|
- [ ] Validate business requirements`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create test template files
|
||||||
|
await fs.writeFile(
|
||||||
|
join(templatesDir, "prd-tmpl.md"),
|
||||||
|
`# Product Requirements Document Template
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
[Brief description of the product]
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
[Detailed requirements here]`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize server
|
||||||
|
server = new BmadMcpServer();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
// Clean up temp directory
|
||||||
|
await fs.rm(tempDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Resource Provider Integration", () => {
|
||||||
|
it("should create ResourceProvider instances for each resource type", () => {
|
||||||
|
expect(server).toBeDefined();
|
||||||
|
expect(server).toBeInstanceOf(BmadMcpServer);
|
||||||
|
|
||||||
|
// Verify server was constructed successfully
|
||||||
|
const resourceProviders = (server as any).resourceProviders;
|
||||||
|
expect(resourceProviders).toBeDefined();
|
||||||
|
expect(resourceProviders.has("Template")).toBe(true);
|
||||||
|
expect(resourceProviders.has("Checklist")).toBe(true);
|
||||||
|
expect(resourceProviders.has("Agent")).toBe(true);
|
||||||
|
expect(resourceProviders.has("Data")).toBe(true);
|
||||||
|
expect(resourceProviders.has("Task")).toBe(true);
|
||||||
|
expect(resourceProviders.has("Util")).toBe(true);
|
||||||
|
expect(resourceProviders.has("Workflow")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have resource configurations with proper settings", () => {
|
||||||
|
const resourceConfigs = (server as any).resourceConfigs;
|
||||||
|
|
||||||
|
expect(resourceConfigs).toHaveLength(7); // Agent, Checklist, Data, Task, Template, Util, Workflow
|
||||||
|
|
||||||
|
const templateConfig = resourceConfigs.find((c: any) => c.resourceType === "Template");
|
||||||
|
expect(templateConfig).toBeDefined();
|
||||||
|
expect(templateConfig.supportedExtensions).toEqual([".md"]);
|
||||||
|
expect(templateConfig.directory).toBe("templates");
|
||||||
|
|
||||||
|
const checklistConfig = resourceConfigs.find((c: any) => c.resourceType === "Checklist");
|
||||||
|
expect(checklistConfig).toBeDefined();
|
||||||
|
expect(checklistConfig.supportedExtensions).toEqual([".md"]);
|
||||||
|
expect(checklistConfig.directory).toBe("checklists");
|
||||||
|
|
||||||
|
// Verify some of the other resource types are present
|
||||||
|
const agentConfig = resourceConfigs.find((c: any) => c.resourceType === "Agent");
|
||||||
|
expect(agentConfig).toBeDefined();
|
||||||
|
expect(agentConfig.supportedExtensions).toEqual([".md"]);
|
||||||
|
expect(agentConfig.directory).toBe("agents");
|
||||||
|
|
||||||
|
const workflowConfig = resourceConfigs.find((c: any) => c.resourceType === "Workflow");
|
||||||
|
expect(workflowConfig).toBeDefined();
|
||||||
|
expect(workflowConfig.supportedExtensions).toEqual([".yml"]);
|
||||||
|
expect(workflowConfig.directory).toBe("workflows");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("MIME Type Handling", () => {
|
||||||
|
it("should correctly determine MIME types", () => {
|
||||||
|
const getMimeType = (server as any).getMimeTypeForExtension.bind(server);
|
||||||
|
|
||||||
|
expect(getMimeType("md")).toBe("text/markdown");
|
||||||
|
expect(getMimeType("txt")).toBe("text/plain");
|
||||||
|
expect(getMimeType("json")).toBe("application/json");
|
||||||
|
expect(getMimeType("unknown")).toBe("text/plain");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||||
|
import { BmadMcpServer } from '../src/server.js';
|
||||||
|
|
||||||
|
// Mock the stdio transport to avoid actual process interaction during tests
|
||||||
|
vi.mock('@modelcontextprotocol/sdk/server/stdio.js', () => ({
|
||||||
|
StdioServerTransport: vi.fn().mockImplementation(() => ({
|
||||||
|
start: vi.fn(),
|
||||||
|
on: vi.fn(),
|
||||||
|
close: vi.fn(),
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock the MCP server
|
||||||
|
vi.mock('@modelcontextprotocol/sdk/server/mcp.js', () => ({
|
||||||
|
McpServer: vi.fn().mockImplementation((config) => ({
|
||||||
|
connect: vi.fn().mockResolvedValue(undefined),
|
||||||
|
close: vi.fn().mockResolvedValue(undefined),
|
||||||
|
resource: vi.fn().mockReturnValue({ enable: vi.fn(), disable: vi.fn() }),
|
||||||
|
config
|
||||||
|
})),
|
||||||
|
ResourceTemplate: vi.fn().mockImplementation((pattern, callbacks) => ({
|
||||||
|
uriTemplate: pattern,
|
||||||
|
listCallback: callbacks.list,
|
||||||
|
completeCallback: vi.fn()
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock the template provider
|
||||||
|
vi.mock('../src/providers/templateProvider.js', () => ({
|
||||||
|
TemplateProvider: vi.fn().mockImplementation(() => ({
|
||||||
|
discoverTemplates: vi.fn().mockResolvedValue([]),
|
||||||
|
getTemplate: vi.fn(),
|
||||||
|
getAllTemplates: vi.fn(),
|
||||||
|
clearCache: vi.fn()
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('BmadMcpServer', () => {
|
||||||
|
let consoleErrorSpy: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
consoleErrorSpy.mockRestore();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('should create server instance with correct configuration', () => {
|
||||||
|
const server = new BmadMcpServer();
|
||||||
|
expect(server).toBeDefined();
|
||||||
|
expect(server).toBeInstanceOf(BmadMcpServer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
|
||||||
|
// Global test setup and configuration
|
||||||
|
beforeEach(() => {
|
||||||
|
// Clear all mocks before each test
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Clean up after each test
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure test timeout globally
|
||||||
|
export const TEST_TIMEOUT = 10000;
|
||||||
|
|
||||||
|
// Mock utilities for MCP transport
|
||||||
|
export const createMockTransport = () => ({
|
||||||
|
start: vi.fn().mockResolvedValue(undefined),
|
||||||
|
close: vi.fn().mockResolvedValue(undefined),
|
||||||
|
send: vi.fn().mockResolvedValue(undefined),
|
||||||
|
onMessage: vi.fn(),
|
||||||
|
onClose: vi.fn(),
|
||||||
|
onError: vi.fn()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock utilities for MCP server
|
||||||
|
export const createMockMcpServer = (config?: any) => ({
|
||||||
|
connect: vi.fn().mockResolvedValue(undefined),
|
||||||
|
close: vi.fn().mockResolvedValue(undefined),
|
||||||
|
setRequestHandler: vi.fn(),
|
||||||
|
setNotificationHandler: vi.fn(),
|
||||||
|
config: config || {
|
||||||
|
name: 'test-server',
|
||||||
|
version: '0.1.0'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Suppress console outputs during tests unless needed
|
||||||
|
const originalConsole = global.console;
|
||||||
|
global.console = {
|
||||||
|
...originalConsole,
|
||||||
|
log: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
debug: vi.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Restore console for specific tests that need it
|
||||||
|
export const restoreConsole = () => {
|
||||||
|
global.console = originalConsole;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mockConsole = () => {
|
||||||
|
global.console = {
|
||||||
|
...originalConsole,
|
||||||
|
log: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
debug: vi.fn()
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist", "tests"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
import dts from 'vite-plugin-dts';
|
||||||
|
import { cpSync } from 'fs';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
dts({
|
||||||
|
insertTypesEntry: true,
|
||||||
|
rollupTypes: true
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'copy-bmad-core',
|
||||||
|
writeBundle() {
|
||||||
|
cpSync('../bmad-core', 'dist/bmad-core', { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
build: {
|
||||||
|
target: 'node18',
|
||||||
|
outDir: 'dist',
|
||||||
|
lib: {
|
||||||
|
entry: resolve(__dirname, 'src/index.ts'),
|
||||||
|
name: 'BmadMcp',
|
||||||
|
fileName: 'index',
|
||||||
|
formats: ['es']
|
||||||
|
},
|
||||||
|
minify: false,
|
||||||
|
sourcemap: true,
|
||||||
|
rollupOptions: {
|
||||||
|
external: [
|
||||||
|
'@modelcontextprotocol/sdk',
|
||||||
|
'node:fs',
|
||||||
|
'node:path',
|
||||||
|
'node:url',
|
||||||
|
'fs',
|
||||||
|
'path',
|
||||||
|
'url'
|
||||||
|
],
|
||||||
|
output: {
|
||||||
|
preserveModules: false,
|
||||||
|
globals: {
|
||||||
|
'@modelcontextprotocol/sdk': 'McpSdk'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emptyOutDir: true
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': resolve(__dirname, 'src')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
global: 'globalThis'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: 'node',
|
||||||
|
globals: true,
|
||||||
|
include: ['tests/**/*.test.ts', '**/*.spec.ts'],
|
||||||
|
coverage: {
|
||||||
|
provider: 'v8',
|
||||||
|
reporter: ['text', 'json', 'html'],
|
||||||
|
exclude: [
|
||||||
|
'node_modules/',
|
||||||
|
'dist/',
|
||||||
|
'coverage/',
|
||||||
|
'*.config.ts',
|
||||||
|
'*.config.js',
|
||||||
|
'src/vite-env.d.ts',
|
||||||
|
'src/providers/',
|
||||||
|
'src/types/index.ts',
|
||||||
|
'src/index.ts'
|
||||||
|
],
|
||||||
|
thresholds: {
|
||||||
|
lines: 80,
|
||||||
|
functions: 80,
|
||||||
|
branches: 80,
|
||||||
|
statements: 80
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setupFiles: ['./tests/setup.ts'],
|
||||||
|
testTimeout: 10000,
|
||||||
|
hookTimeout: 10000
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': resolve(__dirname, 'src')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue