@mofaggolhoshen/dev-assist-mcp 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/mcp.json +10 -0
- package/README.md +100 -0
- package/dist/cli.js +44 -0
- package/dist/index.js +24 -0
- package/dist/tools/fs/index.js +3 -0
- package/dist/tools/fs/listFiles.js +30 -0
- package/dist/tools/fs/readFile.js +34 -0
- package/dist/tools/fs/safePath.js +10 -0
- package/dist/tools/fs/searchCode.js +53 -0
- package/dist/tools/intelligence/analyzeProject.js +127 -0
- package/dist/tools/intelligence/index.js +1 -0
- package/dist/tools/knowledge/getSnippet.js +64 -0
- package/dist/tools/knowledge/index.js +1 -0
- package/dist/tools/ping.js +7 -0
- package/dist/tools/registry.js +15 -0
- package/dist/tools/types.js +1 -0
- package/package.json +44 -0
- package/snippets/efcore-repository.json +7 -0
- package/snippets/jwt-setup.json +7 -0
- package/snippets/polly-retry.json +7 -0
package/.vscode/mcp.json
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# DevAssist MCP Server
|
|
2
|
+
|
|
3
|
+
stdio-based MCP server in TypeScript that exposes project file access, reusable snippets, and lightweight project analysis tools for AI clients.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
1. Install dependencies:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
1. Build the server:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm run build
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
1. Run locally (compiled):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm start
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
1. Or run in development mode:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm run dev
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## VS Code MCP Configuration
|
|
32
|
+
|
|
33
|
+
You can run the MCP server in VS Code either from this repository or from the published npm package.
|
|
34
|
+
|
|
35
|
+
Local workspace setup:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"servers": {
|
|
40
|
+
"dev-assist": {
|
|
41
|
+
"type": "stdio",
|
|
42
|
+
"command": "node",
|
|
43
|
+
"args": ["dist/index.js"],
|
|
44
|
+
"cwd": "${workspaceFolder}"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Build once with `npm run build`, then VS Code can launch the server through stdio.
|
|
51
|
+
|
|
52
|
+
Published npm package setup:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"servers": {
|
|
57
|
+
"dev-assist": {
|
|
58
|
+
"type": "stdio",
|
|
59
|
+
"command": "npx",
|
|
60
|
+
"args": ["-y", "@mofaggolhoshen/dev-assist-mcp-server"]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
This package-based setup is useful when you do not want to build the server from source inside each workspace.
|
|
67
|
+
|
|
68
|
+
## Tool Catalog
|
|
69
|
+
|
|
70
|
+
| Tool | Description | Input |
|
|
71
|
+
| --- | --- | --- |
|
|
72
|
+
| `ping` | Returns pong to verify the server is reachable | `{}` |
|
|
73
|
+
| `list_files` | Lists files recursively under a directory | `{ path?: string }` |
|
|
74
|
+
| `read_file` | Reads a text file from the workspace | `{ path: string }` |
|
|
75
|
+
| `search_code` | Keyword search across text-like files | `{ keyword: string, path?: string }` |
|
|
76
|
+
| `get_snippet` | Returns a named snippet from `snippets/` | `{ name: string }` |
|
|
77
|
+
| `analyze_project` | Detects language/framework/architecture hints | `{ path?: string }` |
|
|
78
|
+
|
|
79
|
+
## Adding Custom Tools
|
|
80
|
+
|
|
81
|
+
1. Create a new tool module under `src/tools/...` implementing the `Tool` interface from `src/tools/types.ts`.
|
|
82
|
+
2. Define `name`, `description`, `inputSchema` (Zod), and `execute`.
|
|
83
|
+
3. Register the tool in `src/index.ts` using `registry.register(yourTool)`.
|
|
84
|
+
4. Rebuild with `npm run build`.
|
|
85
|
+
|
|
86
|
+
## Snippet Authoring
|
|
87
|
+
|
|
88
|
+
Add JSON files under `snippets/` using this schema:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"name": "snippet-name",
|
|
93
|
+
"title": "Human Friendly Title",
|
|
94
|
+
"language": "csharp",
|
|
95
|
+
"description": "What this snippet does",
|
|
96
|
+
"code": "...source code..."
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The `name` must match `[A-Za-z0-9-]+` because it maps to `snippets/{name}.json`.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
const args = process.argv.slice(2);
|
|
4
|
+
async function getPackageVersion() {
|
|
5
|
+
try {
|
|
6
|
+
const packagePath = new URL("../package.json", import.meta.url);
|
|
7
|
+
const raw = await fs.readFile(packagePath, "utf-8");
|
|
8
|
+
const pkg = JSON.parse(raw);
|
|
9
|
+
return pkg.version ?? "unknown";
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return "unknown";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
16
|
+
console.log(`dev-assist-mcp-server
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
npx -y @mofaggolhoshen/dev-assist-mcp-server
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
-h, --help Show this help message
|
|
23
|
+
-v, --version Show package version
|
|
24
|
+
|
|
25
|
+
Description:
|
|
26
|
+
Starts the DevAssist MCP server over stdio for MCP clients.
|
|
27
|
+
|
|
28
|
+
VS Code MCP setup:
|
|
29
|
+
{
|
|
30
|
+
"servers": {
|
|
31
|
+
"dev-assist": {
|
|
32
|
+
"type": "stdio",
|
|
33
|
+
"command": "npx",
|
|
34
|
+
"args": ["-y", "@mofaggolhoshen/dev-assist-mcp-server"]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}`);
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
41
|
+
console.log(await getPackageVersion());
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
await import("./index.js");
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { ToolRegistry } from "./tools/registry.js";
|
|
4
|
+
import { listFilesTool } from "./tools/fs/listFiles.js";
|
|
5
|
+
import { readFileTool } from "./tools/fs/readFile.js";
|
|
6
|
+
import { searchCodeTool } from "./tools/fs/searchCode.js";
|
|
7
|
+
import { getSnippetTool } from "./tools/knowledge/getSnippet.js";
|
|
8
|
+
import { analyzeProjectTool } from "./tools/intelligence/analyzeProject.js";
|
|
9
|
+
const server = new McpServer({ name: "dev-assist-mcp", version: "1.0.0" }, { capabilities: { tools: {} } });
|
|
10
|
+
const registry = new ToolRegistry();
|
|
11
|
+
//registry.register(pingTool);
|
|
12
|
+
registry.register(listFilesTool);
|
|
13
|
+
registry.register(readFileTool);
|
|
14
|
+
registry.register(searchCodeTool);
|
|
15
|
+
registry.register(getSnippetTool);
|
|
16
|
+
registry.register(analyzeProjectTool);
|
|
17
|
+
for (const tool of registry.list()) {
|
|
18
|
+
server.registerTool(tool.name, {
|
|
19
|
+
description: tool.description,
|
|
20
|
+
inputSchema: tool.inputSchema,
|
|
21
|
+
}, async (args) => tool.execute(args));
|
|
22
|
+
}
|
|
23
|
+
const transport = new StdioServerTransport();
|
|
24
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { glob } from "glob";
|
|
3
|
+
import { resolveSafePath } from "./safePath.js";
|
|
4
|
+
export const listFilesTool = {
|
|
5
|
+
name: "list_files",
|
|
6
|
+
description: "Lists files in a directory tree (relative to project root)",
|
|
7
|
+
inputSchema: z.object({
|
|
8
|
+
path: z
|
|
9
|
+
.string()
|
|
10
|
+
.optional()
|
|
11
|
+
.describe("Subdirectory to list (default: project root)"),
|
|
12
|
+
}),
|
|
13
|
+
async execute(input) {
|
|
14
|
+
const relPath = input.path || ".";
|
|
15
|
+
const dir = resolveSafePath(relPath);
|
|
16
|
+
const files = glob.sync("**/*", {
|
|
17
|
+
cwd: dir,
|
|
18
|
+
ignore: ["node_modules/**", "dist/**", ".git/**"],
|
|
19
|
+
nodir: true,
|
|
20
|
+
});
|
|
21
|
+
return {
|
|
22
|
+
content: [
|
|
23
|
+
{
|
|
24
|
+
type: "text",
|
|
25
|
+
text: files.join("\n"),
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import { resolveSafePath } from "./safePath.js";
|
|
4
|
+
export const readFileTool = {
|
|
5
|
+
name: "read_file",
|
|
6
|
+
description: "Reads the content of a text file (relative to project root)",
|
|
7
|
+
inputSchema: z.object({
|
|
8
|
+
path: z.string().describe("Relative path to the file to read"),
|
|
9
|
+
}),
|
|
10
|
+
async execute(input) {
|
|
11
|
+
try {
|
|
12
|
+
const filePath = resolveSafePath(input.path);
|
|
13
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
14
|
+
return {
|
|
15
|
+
content: [
|
|
16
|
+
{
|
|
17
|
+
type: "text",
|
|
18
|
+
text: content,
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
return {
|
|
25
|
+
content: [
|
|
26
|
+
{
|
|
27
|
+
type: "text",
|
|
28
|
+
text: `Error reading file: ${err.message}`,
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
const PROJECT_ROOT = process.cwd();
|
|
3
|
+
export function resolveSafePath(relativePath) {
|
|
4
|
+
const resolved = path.resolve(PROJECT_ROOT, relativePath);
|
|
5
|
+
const relative = path.relative(PROJECT_ROOT, resolved);
|
|
6
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
7
|
+
throw new Error("Access denied: path is outside the project root");
|
|
8
|
+
}
|
|
9
|
+
return resolved;
|
|
10
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { glob } from "glob";
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { resolveSafePath } from "./safePath.js";
|
|
6
|
+
function isTextFile(filename) {
|
|
7
|
+
// Simple extension check; can be improved
|
|
8
|
+
return /\.(ts|js|json|md|txt|c?jsx?|tsx?)$/i.test(filename);
|
|
9
|
+
}
|
|
10
|
+
export const searchCodeTool = {
|
|
11
|
+
name: "search_code",
|
|
12
|
+
description: "Searches for a keyword in text files (relative to project root)",
|
|
13
|
+
inputSchema: z.object({
|
|
14
|
+
keyword: z.string().describe("Text to search for in file contents"),
|
|
15
|
+
path: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("Subdirectory to scope the search (default: project root)"),
|
|
19
|
+
}),
|
|
20
|
+
async execute(input) {
|
|
21
|
+
const relPath = input.path || ".";
|
|
22
|
+
const dir = resolveSafePath(relPath);
|
|
23
|
+
const files = glob.sync("**/*", {
|
|
24
|
+
cwd: dir,
|
|
25
|
+
ignore: ["node_modules/**", "dist/**", ".git/**"],
|
|
26
|
+
nodir: true,
|
|
27
|
+
});
|
|
28
|
+
const matches = [];
|
|
29
|
+
for (const file of files) {
|
|
30
|
+
if (!isTextFile(file))
|
|
31
|
+
continue;
|
|
32
|
+
try {
|
|
33
|
+
const content = await fs.readFile(path.join(dir, file), "utf-8");
|
|
34
|
+
content.split(/\r?\n/).forEach((line, idx) => {
|
|
35
|
+
if (line.toLowerCase().includes(input.keyword.toLowerCase())) {
|
|
36
|
+
matches.push(`${file}:${idx + 1}: ${line}`);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
if (matches.length >= 50)
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
catch { }
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
content: [
|
|
46
|
+
{
|
|
47
|
+
type: "text",
|
|
48
|
+
text: matches.slice(0, 50).join("\n"),
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { glob } from "glob";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { resolveSafePath } from "../fs/safePath.js";
|
|
6
|
+
async function fileExists(filePath) {
|
|
7
|
+
try {
|
|
8
|
+
await fs.access(filePath);
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
async function detectFromPackageJson(projectDir, result) {
|
|
16
|
+
const packageJsonPath = path.join(projectDir, "package.json");
|
|
17
|
+
if (!(await fileExists(packageJsonPath))) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (!result.language.includes("JavaScript")) {
|
|
21
|
+
result.language.push("JavaScript");
|
|
22
|
+
}
|
|
23
|
+
if (!result.framework.includes("Node.js")) {
|
|
24
|
+
result.framework.push("Node.js");
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const raw = await fs.readFile(packageJsonPath, "utf-8");
|
|
28
|
+
const pkg = JSON.parse(raw);
|
|
29
|
+
const deps = {
|
|
30
|
+
...(pkg.dependencies ?? {}),
|
|
31
|
+
...(pkg.devDependencies ?? {}),
|
|
32
|
+
};
|
|
33
|
+
if (deps.typescript && !result.language.includes("TypeScript")) {
|
|
34
|
+
result.language.push("TypeScript");
|
|
35
|
+
}
|
|
36
|
+
if (deps["@modelcontextprotocol/sdk"] &&
|
|
37
|
+
!result.framework.includes("MCP SDK")) {
|
|
38
|
+
result.framework.push("MCP SDK");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Skip malformed package.json and continue returning partial analysis.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export const analyzeProjectTool = {
|
|
46
|
+
name: "analyze_project",
|
|
47
|
+
description: "Analyzes project structure to detect language, framework, and architecture hints",
|
|
48
|
+
inputSchema: z.object({
|
|
49
|
+
path: z
|
|
50
|
+
.string()
|
|
51
|
+
.optional()
|
|
52
|
+
.describe("Project root to analyze (default: server working directory)"),
|
|
53
|
+
}),
|
|
54
|
+
async execute(input) {
|
|
55
|
+
const relPath = input.path || ".";
|
|
56
|
+
const projectDir = resolveSafePath(relPath);
|
|
57
|
+
const result = {
|
|
58
|
+
language: [],
|
|
59
|
+
framework: [],
|
|
60
|
+
architecture: [],
|
|
61
|
+
containerized: false,
|
|
62
|
+
multi_container: false,
|
|
63
|
+
files_count: 0,
|
|
64
|
+
};
|
|
65
|
+
const rootEntries = await fs.readdir(projectDir, { withFileTypes: true });
|
|
66
|
+
const rootNames = new Set(rootEntries.map((entry) => entry.name));
|
|
67
|
+
const csprojFiles = glob.sync("**/*.csproj", {
|
|
68
|
+
cwd: projectDir,
|
|
69
|
+
ignore: ["node_modules/**", "dist/**", ".git/**"],
|
|
70
|
+
nodir: true,
|
|
71
|
+
});
|
|
72
|
+
const slnFiles = glob.sync("**/*.sln", {
|
|
73
|
+
cwd: projectDir,
|
|
74
|
+
ignore: ["node_modules/**", "dist/**", ".git/**"],
|
|
75
|
+
nodir: true,
|
|
76
|
+
});
|
|
77
|
+
const allFiles = glob.sync("**/*", {
|
|
78
|
+
cwd: projectDir,
|
|
79
|
+
ignore: ["node_modules/**", "dist/**", ".git/**"],
|
|
80
|
+
nodir: true,
|
|
81
|
+
});
|
|
82
|
+
result.files_count = allFiles.length;
|
|
83
|
+
if (csprojFiles.length > 0 || slnFiles.length > 0) {
|
|
84
|
+
result.language.push("C#/.NET");
|
|
85
|
+
}
|
|
86
|
+
if (rootNames.has("go.mod")) {
|
|
87
|
+
result.language.push("Go");
|
|
88
|
+
}
|
|
89
|
+
if (rootNames.has("requirements.txt") || rootNames.has("pyproject.toml")) {
|
|
90
|
+
result.language.push("Python");
|
|
91
|
+
}
|
|
92
|
+
await detectFromPackageJson(projectDir, result);
|
|
93
|
+
if (rootNames.has("angular.json")) {
|
|
94
|
+
result.framework.push("Angular");
|
|
95
|
+
}
|
|
96
|
+
const hasNextConfig = glob.sync("next.config.*", {
|
|
97
|
+
cwd: projectDir,
|
|
98
|
+
nodir: true,
|
|
99
|
+
}).length > 0;
|
|
100
|
+
if (hasNextConfig) {
|
|
101
|
+
result.framework.push("Next.js");
|
|
102
|
+
}
|
|
103
|
+
const hasViteConfig = glob.sync("vite.config.*", {
|
|
104
|
+
cwd: projectDir,
|
|
105
|
+
nodir: true,
|
|
106
|
+
}).length > 0;
|
|
107
|
+
if (hasViteConfig) {
|
|
108
|
+
result.framework.push("Vite");
|
|
109
|
+
}
|
|
110
|
+
if (rootNames.has("Program.cs") && csprojFiles.length > 0) {
|
|
111
|
+
result.framework.push("ASP.NET Core");
|
|
112
|
+
}
|
|
113
|
+
result.containerized = rootNames.has("Dockerfile");
|
|
114
|
+
result.multi_container =
|
|
115
|
+
rootNames.has("docker-compose.yml") ||
|
|
116
|
+
rootNames.has("docker-compose.yaml");
|
|
117
|
+
if (rootNames.has("src") &&
|
|
118
|
+
(rootNames.has("tests") || rootNames.has("test"))) {
|
|
119
|
+
result.architecture.push("standard src/test layout");
|
|
120
|
+
}
|
|
121
|
+
if (slnFiles.length > 0 && csprojFiles.length > 1) {
|
|
122
|
+
result.architecture.push("multi-project solution");
|
|
123
|
+
}
|
|
124
|
+
const text = JSON.stringify(result, null, 2);
|
|
125
|
+
return { content: [{ type: "text", text }] };
|
|
126
|
+
},
|
|
127
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./analyzeProject.js";
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { resolveSafePath } from "../fs/safePath.js";
|
|
5
|
+
const snippetFileSchema = z.object({
|
|
6
|
+
name: z.string(),
|
|
7
|
+
title: z.string(),
|
|
8
|
+
language: z.string(),
|
|
9
|
+
description: z.string(),
|
|
10
|
+
code: z.string(),
|
|
11
|
+
});
|
|
12
|
+
async function listAvailableSnippetNames(snippetsDir) {
|
|
13
|
+
try {
|
|
14
|
+
const entries = await fs.readdir(snippetsDir, { withFileTypes: true });
|
|
15
|
+
return entries
|
|
16
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
17
|
+
.map((entry) => entry.name.replace(/\.json$/i, ""))
|
|
18
|
+
.sort();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export const getSnippetTool = {
|
|
25
|
+
name: "get_snippet",
|
|
26
|
+
description: "Returns a reusable code snippet by name from the local snippets directory",
|
|
27
|
+
inputSchema: z.object({
|
|
28
|
+
name: z
|
|
29
|
+
.string()
|
|
30
|
+
.regex(/^[A-Za-z0-9-]+$/, "Snippet name can only contain letters, numbers, and hyphens")
|
|
31
|
+
.describe("Snippet name, e.g. 'polly-retry', 'jwt-setup'"),
|
|
32
|
+
}),
|
|
33
|
+
async execute(input) {
|
|
34
|
+
const snippetsDir = resolveSafePath("snippets");
|
|
35
|
+
const filePath = path.join(snippetsDir, `${input.name}.json`);
|
|
36
|
+
try {
|
|
37
|
+
const raw = await fs.readFile(filePath, "utf-8");
|
|
38
|
+
const snippet = snippetFileSchema.parse(JSON.parse(raw));
|
|
39
|
+
const text = [
|
|
40
|
+
`# ${snippet.title}`,
|
|
41
|
+
"",
|
|
42
|
+
snippet.description,
|
|
43
|
+
"",
|
|
44
|
+
`Language: ${snippet.language}`,
|
|
45
|
+
"",
|
|
46
|
+
`\`\`\`${snippet.language}`,
|
|
47
|
+
snippet.code,
|
|
48
|
+
"\`\`\`",
|
|
49
|
+
].join("\n");
|
|
50
|
+
return { content: [{ type: "text", text }] };
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
const available = await listAvailableSnippetNames(snippetsDir);
|
|
54
|
+
const availableText = available.length
|
|
55
|
+
? available.join(", ")
|
|
56
|
+
: "No snippets found. Add JSON files under snippets/.";
|
|
57
|
+
const notFound = err?.code === "ENOENT";
|
|
58
|
+
const text = notFound
|
|
59
|
+
? `Snippet '${input.name}' was not found. Available snippets: ${availableText}`
|
|
60
|
+
: `Could not load snippet '${input.name}': ${err.message}. Available snippets: ${availableText}`;
|
|
61
|
+
return { content: [{ type: "text", text }] };
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./getSnippet.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class ToolRegistry {
|
|
2
|
+
tools = new Map();
|
|
3
|
+
register(tool) {
|
|
4
|
+
if (this.tools.has(tool.name)) {
|
|
5
|
+
throw new Error(`Tool with name '${tool.name}' is already registered.`);
|
|
6
|
+
}
|
|
7
|
+
this.tools.set(tool.name, tool);
|
|
8
|
+
}
|
|
9
|
+
list() {
|
|
10
|
+
return Array.from(this.tools.values());
|
|
11
|
+
}
|
|
12
|
+
get(name) {
|
|
13
|
+
return this.tools.get(name);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mofaggolhoshen/dev-assist-mcp",
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"dev-assist-mcp": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"directories": {
|
|
10
|
+
"doc": "docs"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"start": "node dist/index.js",
|
|
15
|
+
"dev": "tsx src/index.ts",
|
|
16
|
+
"test": "jest"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [],
|
|
19
|
+
"author": "",
|
|
20
|
+
"license": "ISC",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/MofaggolHoshen/dev-assist-mcp-server.git"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/MofaggolHoshen/dev-assist-mcp-server/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/MofaggolHoshen/dev-assist-mcp-server#readme",
|
|
29
|
+
"type": "module",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
32
|
+
"glob": "^13.0.6",
|
|
33
|
+
"zod": "^4.3.6",
|
|
34
|
+
"zod-to-json-schema": "^3.25.2"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/jest": "^30.0.0",
|
|
38
|
+
"@types/node": "^25.6.0",
|
|
39
|
+
"jest": "^30.3.0",
|
|
40
|
+
"ts-jest": "^29.4.9",
|
|
41
|
+
"tsx": "^4.21.0",
|
|
42
|
+
"typescript": "^6.0.3"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "efcore-repository",
|
|
3
|
+
"title": "EF Core Repository Pattern",
|
|
4
|
+
"language": "csharp",
|
|
5
|
+
"description": "Simple EF Core repository with asynchronous read/write operations.",
|
|
6
|
+
"code": "public sealed class ProductRepository(AppDbContext db)\n{\n public Task<Product?> GetByIdAsync(Guid id, CancellationToken ct = default) =>\n db.Products.FirstOrDefaultAsync(p => p.Id == id, ct);\n\n public async Task AddAsync(Product entity, CancellationToken ct = default)\n {\n await db.Products.AddAsync(entity, ct);\n await db.SaveChangesAsync(ct);\n }\n}"
|
|
7
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jwt-setup",
|
|
3
|
+
"title": "JWT Authentication Setup (ASP.NET Core)",
|
|
4
|
+
"language": "csharp",
|
|
5
|
+
"description": "Configures JWT bearer authentication with token validation parameters.",
|
|
6
|
+
"code": "builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)\n .AddJwtBearer(options =>\n {\n options.TokenValidationParameters = new TokenValidationParameters\n {\n ValidateIssuer = true,\n ValidateAudience = true,\n ValidateIssuerSigningKey = true,\n ValidIssuer = config[\"Jwt:Issuer\"],\n ValidAudience = config[\"Jwt:Audience\"],\n IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config[\"Jwt:Key\"]!))\n };\n });"
|
|
7
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "polly-retry",
|
|
3
|
+
"title": "Polly Retry Policy (.NET)",
|
|
4
|
+
"language": "csharp",
|
|
5
|
+
"description": "Configures an HTTP retry policy with exponential backoff using Polly.",
|
|
6
|
+
"code": "builder.Services.AddHttpClient(\"MyClient\")\n .AddTransientHttpErrorPolicy(p =>\n p.WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt))));"
|
|
7
|
+
}
|