# Directory Structure
```
├── .github
│ └── workflows
│ └── publish.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│ └── index.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | node_modules/
2 | build/
3 | *.log
4 | .env*
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # MCP Function App Tester
2 | [](https://opensource.org/licenses/MIT)
3 |
4 | A TypeScript-based MCP server that enables testing of Azure Function Apps through Cline. This tool allows you to test and interact with Function App endpoints directly from your development environment.
5 |
6 | <a href="https://glama.ai/mcp/servers/la0u86zue0">
7 | <img width="380" height="200" src="https://glama.ai/mcp/servers/la0u86zue0/badge" />
8 | </a>
9 |
10 | ## Installation
11 |
12 | ```bash
13 | npm install dkmaker-mcp-function-app-tester
14 | ```
15 |
16 | ## Features
17 |
18 | - Test Function App endpoints with different HTTP methods
19 | - Support for GET, POST, PUT, and DELETE requests
20 | - Detailed response information
21 | - Custom header support
22 | - Request body handling for POST/PUT methods
23 | - Authentication support:
24 | - Basic Authentication (username/password)
25 | - Bearer Token Authentication
26 | - API Key Authentication (custom header)
27 |
28 | ## Authentication
29 |
30 | The server supports two authentication methods that can be configured via environment variables:
31 |
32 | ### Basic Authentication
33 | Set both environment variables to enable Basic Authentication:
34 | ```bash
35 | AUTH_BASIC_USERNAME=your-username
36 | AUTH_BASIC_PASSWORD=your-password
37 | ```
38 |
39 | ### Bearer Token
40 | Set this environment variable to enable Bearer Token authentication:
41 | ```bash
42 | AUTH_BEARER=your-token
43 | ```
44 |
45 | ### API Key
46 | Set both environment variables to enable API Key authentication:
47 | ```bash
48 | AUTH_APIKEY_HEADER_NAME=X-API-Key # The header name to use (e.g., X-API-Key, api-key, etc.)
49 | AUTH_APIKEY_VALUE=your-api-key # The actual API key value
50 | ```
51 |
52 | Note: Authentication precedence order:
53 | 1. Basic Authentication (if username and password are set)
54 | 2. Bearer Token (if token is set and Basic Auth is not configured)
55 | 3. API Key (if header name and value are set, and no other auth is configured)
56 |
57 | ## Usage
58 |
59 | Once installed, you can use the Function App Tester through Cline. The server provides tools to test endpoints at the base URL: `http://localhost:7071/api`
60 |
61 | Example usage:
62 |
63 | ```typescript
64 | // Test a GET endpoint
65 | {
66 | "method": "GET",
67 | "endpoint": "/users"
68 | }
69 |
70 | // Test a POST endpoint with body
71 | {
72 | "method": "POST",
73 | "endpoint": "/users",
74 | "body": {
75 | "name": "John Doe",
76 | "email": "[email protected]"
77 | }
78 | }
79 |
80 | // Test with custom headers
81 | {
82 | "method": "GET",
83 | "endpoint": "/secure/data",
84 | "headers": {
85 | "Authorization": "Bearer token123"
86 | }
87 | }
88 | ```
89 |
90 | ## Development
91 |
92 | 1. Clone the repository:
93 | ```bash
94 | git clone https://github.com/dkmaker/mcp-function-app-tester.git
95 | cd mcp-function-app-tester
96 | ```
97 |
98 | 2. Install dependencies:
99 | ```bash
100 | npm install
101 | ```
102 |
103 | 3. Build the project:
104 | ```bash
105 | npm run build
106 | ```
107 |
108 | For development with auto-rebuild:
109 | ```bash
110 | npm run watch
111 | ```
112 |
113 | ## License
114 |
115 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
116 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "Node16",
5 | "moduleResolution": "Node16",
6 | "outDir": "./build",
7 | "rootDir": "./src",
8 | "strict": true,
9 | "esModuleInterop": true,
10 | "skipLibCheck": true,
11 | "forceConsistentCasingInFileNames": true
12 | },
13 | "include": ["src/**/*"],
14 | "exclude": ["node_modules"]
15 | }
16 |
```
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
1 | ## [0.2.0](https://github.com/zenturacp/mcp-function-app-tester/compare/v0.1.0...v0.2.0) (2024-12-21)
2 |
3 |
4 | ### Features
5 |
6 | * add API key authentication support ([7882ae7](https://github.com/zenturacp/mcp-function-app-tester/commit/7882ae7ca09e5a0c066c6e9de256eeabdf536893))
7 |
8 | ## [0.1.0](https://github.com/zenturacp/mcp-function-app-tester/compare/b71da08b67cf0fa61523f2205cd1ba37a2cb4e30...v0.1.0) (2024-12-21)
9 |
10 |
11 | ### Features
12 |
13 | * add automated publishing workflow and version management ([b71da08](https://github.com/zenturacp/mcp-function-app-tester/commit/b71da08b67cf0fa61523f2205cd1ba37a2cb4e30))
14 |
15 |
16 | ### Bug Fixes
17 |
18 | * update GitHub Actions workflow configuration ([5e31b8e](https://github.com/zenturacp/mcp-function-app-tester/commit/5e31b8e61a0be542891584bb15ffe08e20587aa0))
19 |
20 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "dkmaker-mcp-function-app-tester",
3 | "version": "0.2.0",
4 | "description": "A test of responses from a Function App",
5 | "license": "MIT",
6 | "type": "module",
7 | "bin": {
8 | "function-app-tester": "./build/index.js"
9 | },
10 | "files": [
11 | "build",
12 | "README.md",
13 | "LICENSE"
14 | ],
15 | "scripts": {
16 | "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
17 | "prepare": "npm run build",
18 | "watch": "tsc --watch",
19 | "inspector": "npx @modelcontextprotocol/inspector build/index.js"
20 | },
21 | "dependencies": {
22 | "@modelcontextprotocol/sdk": "^1.0.4",
23 | "axios": "^1.7.9"
24 | },
25 | "devDependencies": {
26 | "@types/node": "^20.11.24",
27 | "typescript": "^5.3.3"
28 | },
29 | "keywords": [
30 | "mcp",
31 | "azure",
32 | "cline",
33 | "sonnet",
34 | "assistant",
35 | "development",
36 | "typescript"
37 | ],
38 | "author": "dkmaker",
39 | "repository": {
40 | "type": "git",
41 | "url": "git+https://github.com/dkmaker/mcp-function-app-tester.git"
42 | },
43 | "bugs": {
44 | "url": "https://github.com/dkmaker/mcp-function-app-tester/issues"
45 | },
46 | "homepage": "https://github.com/dkmaker/mcp-function-app-tester#readme",
47 | "engines": {
48 | "node": ">=18.0.0"
49 | }
50 | }
51 |
```
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Publish Package
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | workflow_dispatch:
7 |
8 | permissions:
9 | contents: write
10 | pull-requests: write
11 |
12 | jobs:
13 | publish:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v4
17 | with:
18 | fetch-depth: 0
19 |
20 | - name: Setup Node.js
21 | uses: actions/setup-node@v4
22 | with:
23 | node-version: '18.x'
24 | registry-url: 'https://registry.npmjs.org'
25 |
26 | - name: Install dependencies
27 | run: npm ci
28 |
29 | - name: Conventional Changelog Action
30 | id: changelog
31 | uses: TriPSs/conventional-changelog-action@v6
32 | with:
33 | github-token: ${{ secrets.GITHUB_TOKEN }}
34 | git-message: 'chore(release): {version}'
35 | preset: 'conventionalcommits'
36 | tag-prefix: 'v'
37 | output-file: 'CHANGELOG.md'
38 | skip-version-file: false
39 | skip-commit: false
40 |
41 | - name: Build
42 | run: npm run build
43 |
44 | - name: Create Release
45 | if: steps.changelog.outputs.skipped == 'false'
46 | env:
47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48 | run: |
49 | gh release create v${{ steps.changelog.outputs.version }} \
50 | --title "Release v${{ steps.changelog.outputs.version }}" \
51 | --notes "${{ steps.changelog.outputs.clean_changelog }}"
52 |
53 | - name: Publish to NPM
54 | if: steps.changelog.outputs.skipped == 'false'
55 | run: npm publish
56 | env:
57 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
58 |
59 | - name: Push changes
60 | if: steps.changelog.outputs.skipped == 'false'
61 | env:
62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
63 | run: |
64 | git config --global user.name 'github-actions[bot]'
65 | git config --global user.email 'github-actions[bot]@users.noreply.github.com'
66 | git add package.json CHANGELOG.md
67 | git commit -m "chore(release): v${{ steps.changelog.outputs.version }}"
68 | git push "https://[email protected]/$GITHUB_REPOSITORY.git" HEAD:main
69 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4 | import {
5 | CallToolRequestSchema,
6 | ErrorCode,
7 | ListToolsRequestSchema,
8 | McpError,
9 | } from '@modelcontextprotocol/sdk/types.js';
10 | import axios, { AxiosRequestConfig, Method } from 'axios';
11 |
12 | const BASE_URL = process.env.FUNCTION_APP_BASE_URL || 'http://localhost:7071/api';
13 | const AUTH_BASIC_USERNAME = process.env.AUTH_BASIC_USERNAME;
14 | const AUTH_BASIC_PASSWORD = process.env.AUTH_BASIC_PASSWORD;
15 | const AUTH_BEARER = process.env.AUTH_BEARER;
16 | const AUTH_APIKEY_HEADER_NAME = process.env.AUTH_APIKEY_HEADER_NAME;
17 | const AUTH_APIKEY_VALUE = process.env.AUTH_APIKEY_VALUE;
18 |
19 | interface TestEndpointArgs {
20 | method: 'GET' | 'POST' | 'PUT' | 'DELETE';
21 | endpoint: string;
22 | body?: any;
23 | headers?: Record<string, string>;
24 | }
25 |
26 | const isValidTestEndpointArgs = (args: any): args is TestEndpointArgs => {
27 | if (typeof args !== 'object' || args === null) return false;
28 | if (!['GET', 'POST', 'PUT', 'DELETE'].includes(args.method)) return false;
29 | if (typeof args.endpoint !== 'string') return false;
30 | if (args.headers !== undefined && typeof args.headers !== 'object') return false;
31 | return true;
32 | };
33 |
34 | const hasBasicAuth = () => AUTH_BASIC_USERNAME && AUTH_BASIC_PASSWORD;
35 | const hasBearerAuth = () => !!AUTH_BEARER;
36 | const hasApiKeyAuth = () => AUTH_APIKEY_HEADER_NAME && AUTH_APIKEY_VALUE;
37 |
38 | class FunctionAppTester {
39 | private server: Server;
40 | private axiosInstance;
41 |
42 | constructor() {
43 | this.server = new Server(
44 | {
45 | name: 'function-app-tester',
46 | version: '0.1.0',
47 | },
48 | {
49 | capabilities: {
50 | tools: {},
51 | },
52 | }
53 | );
54 |
55 | this.axiosInstance = axios.create({
56 | baseURL: BASE_URL,
57 | validateStatus: () => true, // Allow any status code
58 | });
59 |
60 | this.setupToolHandlers();
61 |
62 | this.server.onerror = (error) => console.error('[MCP Error]', error);
63 | process.on('SIGINT', async () => {
64 | await this.server.close();
65 | process.exit(0);
66 | });
67 | }
68 |
69 | private setupToolHandlers() {
70 | this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
71 | tools: [
72 | {
73 | name: 'test_endpoint',
74 | description: `Test a Function App endpoint and get detailed response information. The endpoint will be prepended to the base url which is: ${BASE_URL}`,
75 | inputSchema: {
76 | type: 'object',
77 | properties: {
78 | method: {
79 | type: 'string',
80 | enum: ['GET', 'POST', 'PUT', 'DELETE'],
81 | description: 'HTTP method to use',
82 | },
83 | endpoint: {
84 | type: 'string',
85 | description: 'Endpoint path (e.g. "/users"). Will be appended to base URL.',
86 | },
87 | body: {
88 | type: 'object',
89 | description: 'Optional request body for POST/PUT requests',
90 | },
91 | headers: {
92 | type: 'object',
93 | description: 'Optional request headers',
94 | additionalProperties: {
95 | type: 'string',
96 | },
97 | },
98 | },
99 | required: ['method', 'endpoint'],
100 | },
101 | },
102 | ],
103 | }));
104 |
105 | this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
106 | if (request.params.name !== 'test_endpoint') {
107 | throw new McpError(
108 | ErrorCode.MethodNotFound,
109 | `Unknown tool: ${request.params.name}`
110 | );
111 | }
112 |
113 | if (!isValidTestEndpointArgs(request.params.arguments)) {
114 | throw new McpError(
115 | ErrorCode.InvalidParams,
116 | 'Invalid test endpoint arguments'
117 | );
118 | }
119 |
120 | try {
121 | const config: AxiosRequestConfig = {
122 | method: request.params.arguments.method as Method,
123 | url: request.params.arguments.endpoint,
124 | headers: request.params.arguments.headers || {},
125 | };
126 |
127 | if (['POST', 'PUT'].includes(request.params.arguments.method) && request.params.arguments.body) {
128 | config.data = request.params.arguments.body;
129 | }
130 |
131 | // Handle authentication based on environment variables
132 | if (hasBasicAuth()) {
133 | const base64Credentials = Buffer.from(`${AUTH_BASIC_USERNAME}:${AUTH_BASIC_PASSWORD}`).toString('base64');
134 | config.headers = {
135 | ...config.headers,
136 | 'Authorization': `Basic ${base64Credentials}`
137 | };
138 | } else if (hasBearerAuth()) {
139 | config.headers = {
140 | ...config.headers,
141 | 'Authorization': `Bearer ${AUTH_BEARER}`
142 | };
143 | } else if (hasApiKeyAuth()) {
144 | config.headers = {
145 | ...config.headers,
146 | [AUTH_APIKEY_HEADER_NAME as string]: AUTH_APIKEY_VALUE
147 | };
148 | }
149 |
150 | // Ensure endpoint starts with / and remove any trailing slashes
151 | const normalizedEndpoint = `/${request.params.arguments.endpoint.replace(/^\/+|\/+$/g, '')}`;
152 | config.url = normalizedEndpoint;
153 |
154 | const response = await this.axiosInstance.request(config);
155 | const fullUrl = `${BASE_URL}${normalizedEndpoint}`;
156 |
157 | return {
158 | content: [
159 | {
160 | type: 'text',
161 | text: JSON.stringify({
162 | url: fullUrl, // Include the actual URL called
163 | statusCode: response.status,
164 | statusText: response.statusText,
165 | headers: response.headers,
166 | body: response.data,
167 | }, null, 2),
168 | },
169 | ],
170 | };
171 | } catch (error) {
172 | if (axios.isAxiosError(error)) {
173 | return {
174 | content: [
175 | {
176 | type: 'text',
177 | text: `Request failed: ${error.message}`,
178 | },
179 | ],
180 | isError: true,
181 | };
182 | }
183 | throw error;
184 | }
185 | });
186 | }
187 |
188 | async run() {
189 | const transport = new StdioServerTransport();
190 | await this.server.connect(transport);
191 | console.error('Function App Tester MCP server running on stdio');
192 | }
193 | }
194 |
195 | const server = new FunctionAppTester();
196 | server.run().catch(console.error);
197 |
```