Skip to main content
Glama

documcp

by tosin2013
tools.test.ts28.8 kB
// Functional tests for all MCP tools with real repository scenarios import { promises as fs } from "fs"; import path from "path"; import os from "os"; import { analyzeRepository } from "../../src/tools/analyze-repository"; import { recommendSSG } from "../../src/tools/recommend-ssg"; import { generateConfig } from "../../src/tools/generate-config"; import { setupStructure } from "../../src/tools/setup-structure"; import { deployPages } from "../../src/tools/deploy-pages"; import { verifyDeployment } from "../../src/tools/verify-deployment"; describe("Functional Testing - MCP Tools", () => { let tempDir: string; let testRepos: { javascript: string; python: string; ruby: string; go: string; mixed: string; large: string; empty: string; }; beforeAll(async () => { tempDir = path.join(os.tmpdir(), "documcp-functional-tests"); await fs.mkdir(tempDir, { recursive: true }); testRepos = { javascript: await createJavaScriptRepo(), python: await createPythonRepo(), ruby: await createRubyRepo(), go: await createGoRepo(), mixed: await createMixedLanguageRepo(), large: await createLargeRepo(), empty: await createEmptyRepo(), }; }); afterAll(async () => { try { await fs.rm(tempDir, { recursive: true, force: true }); } catch (error) { console.warn("Failed to cleanup test directory:", error); } }); describe("analyze_repository Tool", () => { it("should analyze JavaScript/TypeScript repository correctly", async () => { const result = await analyzeRepository({ path: testRepos.javascript, depth: "standard", }); expect(result.content).toBeDefined(); expect(result.content.length).toBeGreaterThan(0); // Parse the JSON response to validate structure const analysisText = result.content.find((c) => c.text.includes('"id"')); expect(analysisText).toBeDefined(); const analysis = JSON.parse(analysisText!.text); expect(analysis.dependencies.ecosystem).toBe("javascript"); expect(analysis.structure.languages[".js"]).toBeGreaterThan(0); expect(analysis.documentation.hasReadme).toBe(true); expect(analysis.recommendations.primaryLanguage).toBe("javascript"); }); it("should analyze Python repository correctly", async () => { const result = await analyzeRepository({ path: testRepos.python, depth: "standard", }); const analysisText = result.content.find((c) => c.text.includes('"ecosystem"'), ); const analysis = JSON.parse(analysisText!.text); expect(analysis.dependencies.ecosystem).toBe("python"); expect(analysis.structure.languages[".py"]).toBeGreaterThan(0); expect(analysis.dependencies.packages.length).toBeGreaterThan(0); }); it("should analyze Ruby repository correctly", async () => { const result = await analyzeRepository({ path: testRepos.ruby, depth: "standard", }); const analysisText = result.content.find((c) => c.text.includes('"ecosystem"'), ); const analysis = JSON.parse(analysisText!.text); expect(analysis.dependencies.ecosystem).toBe("ruby"); expect(analysis.structure.languages[".rb"]).toBeGreaterThan(0); }); it("should analyze Go repository correctly", async () => { const result = await analyzeRepository({ path: testRepos.go, depth: "standard", }); const analysisText = result.content.find((c) => c.text.includes('"ecosystem"'), ); const analysis = JSON.parse(analysisText!.text); expect(analysis.dependencies.ecosystem).toBe("go"); expect(analysis.structure.languages[".go"]).toBeGreaterThan(0); }); it("should handle different analysis depths", async () => { const quickResult = await analyzeRepository({ path: testRepos.javascript, depth: "quick", }); const deepResult = await analyzeRepository({ path: testRepos.javascript, depth: "deep", }); expect(quickResult.content).toBeDefined(); expect(deepResult.content).toBeDefined(); // Both should return valid results but potentially different detail levels const quickAnalysis = JSON.parse( quickResult.content.find((c) => c.text.includes('"id"'))!.text, ); const deepAnalysis = JSON.parse( deepResult.content.find((c) => c.text.includes('"id"'))!.text, ); expect(quickAnalysis.id).toBeDefined(); expect(deepAnalysis.id).toBeDefined(); }); it("should handle empty repository gracefully", async () => { const result = await analyzeRepository({ path: testRepos.empty, depth: "standard", }); const analysisText = result.content.find((c) => c.text.includes('"totalFiles"'), ); const analysis = JSON.parse(analysisText!.text); expect(analysis.structure.totalFiles).toBe(1); // Only README.md expect(analysis.dependencies.ecosystem).toBe("unknown"); }); it("should handle non-existent repository path", async () => { const nonExistentPath = path.join(tempDir, "does-not-exist"); const result = await analyzeRepository({ path: nonExistentPath, depth: "standard", }); expect((result as any).isError).toBe(true); expect(result.content[0].text).toContain("Error:"); }); }); describe("recommend_ssg Tool", () => { it("should recommend SSG based on analysis", async () => { const result = await recommendSSG({ analysisId: "test-analysis-123", }); expect(result.content).toBeDefined(); expect(result.content.length).toBeGreaterThan(0); // Should contain recommendation data const recommendationText = result.content.find((c) => c.text.includes('"recommended"'), ); expect(recommendationText).toBeDefined(); const recommendation = JSON.parse(recommendationText!.text); expect(recommendation.recommended).toBeDefined(); expect(recommendation.confidence).toBeGreaterThan(0); expect(recommendation.reasoning).toBeDefined(); expect(recommendation.alternatives).toBeDefined(); }); it("should handle preferences parameter", async () => { const result = await recommendSSG({ analysisId: "test-analysis-456", preferences: { priority: "simplicity", ecosystem: "javascript", }, }); expect(result.content).toBeDefined(); const recommendationText = result.content.find((c) => c.text.includes('"recommended"'), ); const recommendation = JSON.parse(recommendationText!.text); expect(["jekyll", "hugo", "docusaurus", "mkdocs", "eleventy"]).toContain( recommendation.recommended, ); }); }); describe("generate_config Tool", () => { let configOutputDir: string; beforeEach(async () => { configOutputDir = path.join( tempDir, "config-output", Date.now().toString(), ); await fs.mkdir(configOutputDir, { recursive: true }); }); it("should generate Docusaurus configuration", async () => { const result = await generateConfig({ ssg: "docusaurus", projectName: "Test Docusaurus Project", projectDescription: "A test project for Docusaurus", outputPath: configOutputDir, }); expect(result.content).toBeDefined(); // Verify files were created const docusaurusConfig = path.join( configOutputDir, "docusaurus.config.js", ); const packageJson = path.join(configOutputDir, "package.json"); expect( await fs .access(docusaurusConfig) .then(() => true) .catch(() => false), ).toBe(true); expect( await fs .access(packageJson) .then(() => true) .catch(() => false), ).toBe(true); // Verify file contents const configContent = await fs.readFile(docusaurusConfig, "utf-8"); expect(configContent).toContain("Test Docusaurus Project"); expect(configContent).toContain("classic"); }); it("should generate MkDocs configuration", async () => { const result = await generateConfig({ ssg: "mkdocs", projectName: "Test MkDocs Project", outputPath: configOutputDir, }); expect(result.content).toBeDefined(); const mkdocsConfig = path.join(configOutputDir, "mkdocs.yml"); const requirements = path.join(configOutputDir, "requirements.txt"); expect( await fs .access(mkdocsConfig) .then(() => true) .catch(() => false), ).toBe(true); expect( await fs .access(requirements) .then(() => true) .catch(() => false), ).toBe(true); const configContent = await fs.readFile(mkdocsConfig, "utf-8"); expect(configContent).toContain("Test MkDocs Project"); expect(configContent).toContain("material"); }); it("should generate Hugo configuration", async () => { const result = await generateConfig({ ssg: "hugo", projectName: "Test Hugo Project", outputPath: configOutputDir, }); const hugoConfig = path.join(configOutputDir, "hugo.toml"); expect( await fs .access(hugoConfig) .then(() => true) .catch(() => false), ).toBe(true); const configContent = await fs.readFile(hugoConfig, "utf-8"); expect(configContent).toContain("Test Hugo Project"); }); it("should generate Jekyll configuration", async () => { const result = await generateConfig({ ssg: "jekyll", projectName: "Test Jekyll Project", outputPath: configOutputDir, }); const jekyllConfig = path.join(configOutputDir, "_config.yml"); const gemfile = path.join(configOutputDir, "Gemfile"); expect( await fs .access(jekyllConfig) .then(() => true) .catch(() => false), ).toBe(true); expect( await fs .access(gemfile) .then(() => true) .catch(() => false), ).toBe(true); }); it("should generate Eleventy configuration", async () => { const result = await generateConfig({ ssg: "eleventy", projectName: "Test Eleventy Project", outputPath: configOutputDir, }); const eleventyConfig = path.join(configOutputDir, ".eleventy.js"); const packageJson = path.join(configOutputDir, "package.json"); expect( await fs .access(eleventyConfig) .then(() => true) .catch(() => false), ).toBe(true); expect( await fs .access(packageJson) .then(() => true) .catch(() => false), ).toBe(true); }); }); describe("setup_structure Tool", () => { let structureOutputDir: string; beforeEach(async () => { structureOutputDir = path.join( tempDir, "structure-output", Date.now().toString(), ); }); it("should create Diataxis structure with examples", async () => { const result = await setupStructure({ path: structureOutputDir, ssg: "docusaurus", includeExamples: true, }); expect(result.content).toBeDefined(); // Verify directory structure const categories = ["tutorials", "how-to", "reference", "explanation"]; for (const category of categories) { const categoryDir = path.join(structureOutputDir, category); expect( await fs .access(categoryDir) .then(() => true) .catch(() => false), ).toBe(true); // Check for index.md const indexFile = path.join(categoryDir, "index.md"); expect( await fs .access(indexFile) .then(() => true) .catch(() => false), ).toBe(true); // Check for example file const files = await fs.readdir(categoryDir); expect(files.length).toBeGreaterThan(1); // index.md + example file } // Check root index const rootIndex = path.join(structureOutputDir, "index.md"); expect( await fs .access(rootIndex) .then(() => true) .catch(() => false), ).toBe(true); const rootContent = await fs.readFile(rootIndex, "utf-8"); expect(rootContent).toContain("Diataxis"); expect(rootContent).toContain("Tutorials"); expect(rootContent).toContain("How-To Guides"); }); it("should create structure without examples", async () => { const result = await setupStructure({ path: structureOutputDir, ssg: "mkdocs", includeExamples: false, }); expect(result.content).toBeDefined(); // Verify only index files exist (no examples) const tutorialsDir = path.join(structureOutputDir, "tutorials"); const files = await fs.readdir(tutorialsDir); expect(files).toEqual(["index.md"]); // Only index, no example }); it("should handle different SSG formats correctly", async () => { // Test Docusaurus format await setupStructure({ path: path.join(structureOutputDir, "docusaurus"), ssg: "docusaurus", includeExamples: true, }); const docusaurusFile = path.join( structureOutputDir, "docusaurus", "tutorials", "index.md", ); const docusaurusContent = await fs.readFile(docusaurusFile, "utf-8"); expect(docusaurusContent).toContain("id: tutorials-index"); expect(docusaurusContent).toContain("sidebar_label:"); // Test Jekyll format await setupStructure({ path: path.join(structureOutputDir, "jekyll"), ssg: "jekyll", includeExamples: true, }); const jekyllFile = path.join( structureOutputDir, "jekyll", "tutorials", "index.md", ); const jekyllContent = await fs.readFile(jekyllFile, "utf-8"); expect(jekyllContent).toContain("title:"); expect(jekyllContent).toContain("description:"); }); }); describe("deploy_pages Tool", () => { let deploymentRepoDir: string; beforeEach(async () => { deploymentRepoDir = path.join( tempDir, "deployment-repo", Date.now().toString(), ); await fs.mkdir(deploymentRepoDir, { recursive: true }); }); it("should create GitHub Actions workflow for Docusaurus", async () => { const result = await deployPages({ repository: deploymentRepoDir, ssg: "docusaurus", branch: "gh-pages", }); expect(result.content).toBeDefined(); const workflowPath = path.join( deploymentRepoDir, ".github", "workflows", "deploy-docs.yml", ); expect( await fs .access(workflowPath) .then(() => true) .catch(() => false), ).toBe(true); const workflowContent = await fs.readFile(workflowPath, "utf-8"); expect(workflowContent).toContain("Deploy Docusaurus"); expect(workflowContent).toContain("npm run build"); expect(workflowContent).toContain("actions/upload-pages-artifact"); expect(workflowContent).toContain("actions/deploy-pages"); // Verify security compliance (OIDC tokens) expect(workflowContent).toContain("id-token: write"); expect(workflowContent).toContain("pages: write"); expect(workflowContent).not.toContain("GITHUB_TOKEN: ${{ secrets."); }); it("should create workflow for MkDocs", async () => { const result = await deployPages({ repository: deploymentRepoDir, ssg: "mkdocs", }); const workflowPath = path.join( deploymentRepoDir, ".github", "workflows", "deploy-docs.yml", ); const workflowContent = await fs.readFile(workflowPath, "utf-8"); expect(workflowContent).toContain("Deploy MkDocs"); expect(workflowContent).toContain("mkdocs gh-deploy"); expect(workflowContent).toContain("python"); }); it("should create workflow for Hugo", async () => { const result = await deployPages({ repository: deploymentRepoDir, ssg: "hugo", }); const workflowContent = await fs.readFile( path.join(deploymentRepoDir, ".github", "workflows", "deploy-docs.yml"), "utf-8", ); expect(workflowContent).toContain("Deploy Hugo"); expect(workflowContent).toContain("peaceiris/actions-hugo"); expect(workflowContent).toContain("hugo --minify"); }); it("should handle custom domain configuration", async () => { const result = await deployPages({ repository: deploymentRepoDir, ssg: "jekyll", customDomain: "docs.example.com", }); // Check CNAME file creation const cnamePath = path.join(deploymentRepoDir, "CNAME"); expect( await fs .access(cnamePath) .then(() => true) .catch(() => false), ).toBe(true); const cnameContent = await fs.readFile(cnamePath, "utf-8"); expect(cnameContent.trim()).toBe("docs.example.com"); // Verify result indicates custom domain was configured const resultText = result.content.map((c) => c.text).join(" "); expect(resultText).toContain("docs.example.com"); }); }); describe("verify_deployment Tool", () => { let verificationRepoDir: string; beforeEach(async () => { verificationRepoDir = path.join( tempDir, "verification-repo", Date.now().toString(), ); await fs.mkdir(verificationRepoDir, { recursive: true }); }); it("should verify complete deployment setup", async () => { // Set up a complete deployment scenario await fs.mkdir(path.join(verificationRepoDir, ".github", "workflows"), { recursive: true, }); await fs.mkdir(path.join(verificationRepoDir, "docs"), { recursive: true, }); await fs.mkdir(path.join(verificationRepoDir, "build"), { recursive: true, }); // Create workflow file await fs.writeFile( path.join( verificationRepoDir, ".github", "workflows", "deploy-docs.yml", ), "name: Deploy Docs\non: push\njobs:\n deploy:\n runs-on: ubuntu-latest", ); // Create documentation files await fs.writeFile( path.join(verificationRepoDir, "docs", "index.md"), "# Documentation", ); await fs.writeFile( path.join(verificationRepoDir, "docs", "guide.md"), "# Guide", ); // Create config file await fs.writeFile( path.join(verificationRepoDir, "docusaurus.config.js"), 'module.exports = { title: "Test" };', ); // Create build directory await fs.writeFile( path.join(verificationRepoDir, "build", "index.html"), "<h1>Built Site</h1>", ); const result = await verifyDeployment({ repository: verificationRepoDir, url: "https://example.github.io/test-repo", }); expect(result.content).toBeDefined(); // Parse the verification result const verification = JSON.parse(result.content[0].text); expect(verification.summary.passed).toBeGreaterThan(0); // Should have passing checks expect( verification.checks.some((check: any) => check.message.includes("deployment workflow"), ), ).toBe(true); expect( verification.checks.some((check: any) => check.message.includes("documentation files"), ), ).toBe(true); expect( verification.checks.some((check: any) => check.message.includes("configuration"), ), ).toBe(true); expect( verification.checks.some((check: any) => check.message.includes("build output"), ), ).toBe(true); }); it("should identify missing components", async () => { // Create minimal repo without deployment setup await fs.writeFile( path.join(verificationRepoDir, "README.md"), "# Test Repo", ); const result = await verifyDeployment({ repository: verificationRepoDir, }); const verification = JSON.parse(result.content[0].text); expect(verification.summary.failed).toBeGreaterThan(0); // Should have failing checks expect( verification.checks.some((check: any) => check.message.includes("No .github/workflows"), ), ).toBe(true); expect( verification.checks.some((check: any) => check.message.includes("No documentation files"), ), ).toBe(true); expect( verification.checks.some((check: any) => check.message.includes("No static site generator configuration"), ), ).toBe(true); }); it("should provide actionable recommendations", async () => { const result = await verifyDeployment({ repository: verificationRepoDir, }); const resultText = result.content.map((c) => c.text).join("\n"); expect(resultText).toContain("→"); // Should contain recommendation arrows expect(resultText).toContain("deploy_pages tool"); expect(resultText).toContain("setup_structure tool"); expect(resultText).toContain("generate_config tool"); }); it("should handle repository path variations", async () => { // Test with relative path const relativeResult = await verifyDeployment({ repository: ".", }); expect(relativeResult.content).toBeDefined(); // Test with absolute path const absoluteResult = await verifyDeployment({ repository: verificationRepoDir, }); expect(absoluteResult.content).toBeDefined(); // Test with HTTP URL (should default to current directory) const urlResult = await verifyDeployment({ repository: "https://github.com/user/repo", }); expect(urlResult.content).toBeDefined(); }); }); // Helper functions to create test repositories async function createJavaScriptRepo(): Promise<string> { const repoPath = path.join(tempDir, "javascript-repo"); await fs.mkdir(repoPath, { recursive: true }); // package.json const packageJson = { name: "test-js-project", version: "1.0.0", description: "Test JavaScript project", scripts: { start: "node index.js", test: "jest", }, dependencies: { express: "^4.18.0", lodash: "^4.17.21", }, devDependencies: { jest: "^29.0.0", "@types/node": "^20.0.0", }, }; await fs.writeFile( path.join(repoPath, "package.json"), JSON.stringify(packageJson, null, 2), ); // Source files await fs.writeFile( path.join(repoPath, "index.js"), 'console.log("Hello World");', ); await fs.writeFile( path.join(repoPath, "utils.js"), "module.exports = { helper: () => {} };", ); await fs.writeFile( path.join(repoPath, "app.ts"), 'const app: string = "TypeScript";', ); // Test directory await fs.mkdir(path.join(repoPath, "test"), { recursive: true }); await fs.writeFile( path.join(repoPath, "test", "app.test.js"), 'test("example", () => {});', ); // Documentation await fs.writeFile( path.join(repoPath, "README.md"), "# JavaScript Test Project\nA test project for JavaScript analysis.", ); await fs.writeFile( path.join(repoPath, "CONTRIBUTING.md"), "# Contributing\nHow to contribute.", ); await fs.writeFile(path.join(repoPath, "LICENSE"), "MIT License"); // CI/CD await fs.mkdir(path.join(repoPath, ".github", "workflows"), { recursive: true, }); await fs.writeFile( path.join(repoPath, ".github", "workflows", "ci.yml"), "name: CI\non: push\njobs:\n test:\n runs-on: ubuntu-latest", ); return repoPath; } async function createPythonRepo(): Promise<string> { const repoPath = path.join(tempDir, "python-repo"); await fs.mkdir(repoPath, { recursive: true }); // requirements.txt await fs.writeFile( path.join(repoPath, "requirements.txt"), "flask>=2.0.0\nrequests>=2.25.0\nnumpy>=1.21.0", ); // Python files await fs.writeFile( path.join(repoPath, "main.py"), "import flask\napp = flask.Flask(__name__)", ); await fs.writeFile( path.join(repoPath, "utils.py"), "def helper():\n pass", ); // Tests await fs.mkdir(path.join(repoPath, "tests"), { recursive: true }); await fs.writeFile( path.join(repoPath, "tests", "test_main.py"), "def test_app():\n assert True", ); await fs.writeFile( path.join(repoPath, "README.md"), "# Python Test Project", ); return repoPath; } async function createRubyRepo(): Promise<string> { const repoPath = path.join(tempDir, "ruby-repo"); await fs.mkdir(repoPath, { recursive: true }); // Gemfile await fs.writeFile( path.join(repoPath, "Gemfile"), 'source "https://rubygems.org"\ngem "rails"', ); // Ruby files await fs.writeFile(path.join(repoPath, "app.rb"), "class App\nend"); await fs.writeFile(path.join(repoPath, "helper.rb"), "module Helper\nend"); await fs.writeFile(path.join(repoPath, "README.md"), "# Ruby Test Project"); return repoPath; } async function createGoRepo(): Promise<string> { const repoPath = path.join(tempDir, "go-repo"); await fs.mkdir(repoPath, { recursive: true }); // go.mod await fs.writeFile( path.join(repoPath, "go.mod"), "module test-go-project\ngo 1.19", ); // Go files await fs.writeFile( path.join(repoPath, "main.go"), "package main\nfunc main() {}", ); await fs.writeFile( path.join(repoPath, "utils.go"), "package main\nfunc helper() {}", ); await fs.writeFile(path.join(repoPath, "README.md"), "# Go Test Project"); return repoPath; } async function createMixedLanguageRepo(): Promise<string> { const repoPath = path.join(tempDir, "mixed-repo"); await fs.mkdir(repoPath, { recursive: true }); // Multiple language files await fs.writeFile( path.join(repoPath, "package.json"), '{"name": "mixed-project"}', ); await fs.writeFile(path.join(repoPath, "requirements.txt"), "flask>=2.0.0"); await fs.writeFile(path.join(repoPath, "Gemfile"), 'gem "rails"'); await fs.writeFile(path.join(repoPath, "app.js"), 'console.log("JS");'); await fs.writeFile(path.join(repoPath, "script.py"), 'print("Python")'); await fs.writeFile(path.join(repoPath, "server.rb"), 'puts "Ruby"'); await fs.writeFile( path.join(repoPath, "README.md"), "# Mixed Language Project", ); return repoPath; } async function createLargeRepo(): Promise<string> { const repoPath = path.join(tempDir, "large-repo"); await fs.mkdir(repoPath, { recursive: true }); // Create many files to simulate a large repository for (let i = 0; i < 150; i++) { const fileName = `file-${i.toString().padStart(3, "0")}.js`; await fs.writeFile( path.join(repoPath, fileName), `// File ${i}\nconsole.log(${i});`, ); } // Create nested directories for (let i = 0; i < 10; i++) { const dirPath = path.join(repoPath, `dir-${i}`); await fs.mkdir(dirPath, { recursive: true }); for (let j = 0; j < 20; j++) { const fileName = `nested-${j}.js`; await fs.writeFile( path.join(dirPath, fileName), `// Nested file ${i}-${j}`, ); } } await fs.writeFile( path.join(repoPath, "package.json"), '{"name": "large-project"}', ); await fs.writeFile( path.join(repoPath, "README.md"), "# Large Test Project", ); return repoPath; } async function createEmptyRepo(): Promise<string> { const repoPath = path.join(tempDir, "empty-repo"); await fs.mkdir(repoPath, { recursive: true }); // Only a README file await fs.writeFile( path.join(repoPath, "README.md"), "# Empty Project\nMinimal repository for testing.", ); return repoPath; } });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/tosin2013/documcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server