Skip to content

Commit 60a1864

Browse files
mowens3claude
andcommitted
Initial release: MCP HTTP Security v1.0.0
Secure HTTP transport wrapper for MCP servers in PHP. Features: - API Key Management with secure hashing (SHA-256 + pepper) - Key TTL/expiry and revocation - Multiple storage backends (Array, File, PDO) - IP allowlisting with CIDR support (IPv4/IPv6) - Origin/hostname allowlisting with wildcard subdomains - PSR-15 SecurityMiddleware - PSR-20 Clock support Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
0 parents  commit 60a1864

27 files changed

+1946
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/vendor/
2+
/.phpunit.cache/
3+
composer.lock
4+
.phpunit.result.cache

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 CodeWheel
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
# MCP HTTP Security
2+
3+
Secure HTTP transport wrapper for MCP (Model Context Protocol) servers in PHP.
4+
5+
Provides production-ready security components that don't exist elsewhere in the PHP MCP ecosystem:
6+
7+
- **API Key Authentication** - Secure key generation, hashing (SHA-256 + pepper), TTL/expiry
8+
- **IP Allowlisting** - CIDR notation, IPv4/IPv6 support
9+
- **Origin Allowlisting** - Hostname validation with wildcard subdomain support
10+
- **PSR-15 Middleware** - Drop-in security for any PSR-15 compatible framework
11+
12+
## Installation
13+
14+
```bash
15+
composer require code-wheel/mcp-http-security
16+
```
17+
18+
## Quick Start
19+
20+
```php
21+
<?php
22+
23+
use CodeWheel\McpSecurity\ApiKey\ApiKeyManager;
24+
use CodeWheel\McpSecurity\ApiKey\Storage\FileStorage;
25+
use CodeWheel\McpSecurity\Clock\SystemClock;
26+
use CodeWheel\McpSecurity\Config\SecurityConfig;
27+
use CodeWheel\McpSecurity\Middleware\SecurityMiddleware;
28+
use CodeWheel\McpSecurity\Validation\RequestValidator;
29+
30+
// 1. Setup API Key Manager
31+
$storage = new FileStorage('/var/data/mcp-api-keys.json');
32+
$clock = new SystemClock();
33+
$apiKeyManager = new ApiKeyManager(
34+
storage: $storage,
35+
clock: $clock,
36+
pepper: getenv('MCP_API_KEY_PEPPER') ?: '',
37+
);
38+
39+
// 2. Create a key
40+
$result = $apiKeyManager->createKey(
41+
label: 'Claude Code',
42+
scopes: ['read', 'write'],
43+
ttlSeconds: 86400 * 30, // 30 days
44+
);
45+
echo "API Key: {$result['api_key']}\n"; // Show once, store securely
46+
47+
// 3. Setup Request Validator
48+
$validator = new RequestValidator(
49+
allowedIps: ['127.0.0.1', '10.0.0.0/8'],
50+
allowedOrigins: ['localhost', '*.example.com'],
51+
);
52+
53+
// 4. Create Middleware
54+
$middleware = new SecurityMiddleware(
55+
apiKeyManager: $apiKeyManager,
56+
requestValidator: $validator,
57+
responseFactory: new HttpFactory(), // Any PSR-17 factory
58+
config: new SecurityConfig(
59+
requireAuth: true,
60+
allowedScopes: ['read', 'write'],
61+
),
62+
);
63+
64+
// 5. Use with your PSR-15 application
65+
$app->pipe($middleware);
66+
```
67+
68+
## API Key Management
69+
70+
### Creating Keys
71+
72+
```php
73+
$result = $apiKeyManager->createKey(
74+
label: 'Production API',
75+
scopes: ['read', 'write', 'admin'],
76+
ttlSeconds: null, // No expiry
77+
);
78+
79+
// Returns:
80+
// [
81+
// 'key_id' => 'abc123def456',
82+
// 'api_key' => 'mcp.abc123def456.secret...',
83+
// ]
84+
```
85+
86+
### Listing Keys
87+
88+
```php
89+
$keys = $apiKeyManager->listKeys();
90+
91+
foreach ($keys as $keyId => $key) {
92+
echo "{$key->label} - Scopes: " . implode(', ', $key->scopes) . "\n";
93+
echo " Created: " . date('Y-m-d', $key->created) . "\n";
94+
echo " Expires: " . ($key->expires ? date('Y-m-d', $key->expires) : 'Never') . "\n";
95+
}
96+
```
97+
98+
### Validating Keys
99+
100+
```php
101+
$apiKey = $apiKeyManager->validate($tokenFromRequest);
102+
103+
if ($apiKey === null) {
104+
// Invalid or expired
105+
}
106+
107+
if ($apiKey->hasScope('write')) {
108+
// Allow write operation
109+
}
110+
```
111+
112+
### Revoking Keys
113+
114+
```php
115+
$apiKeyManager->revokeKey('abc123def456');
116+
```
117+
118+
## Storage Backends
119+
120+
### File Storage (Simple)
121+
122+
```php
123+
use CodeWheel\McpSecurity\ApiKey\Storage\FileStorage;
124+
125+
$storage = new FileStorage('/var/data/api-keys.json');
126+
```
127+
128+
### Database Storage (PDO)
129+
130+
```php
131+
use CodeWheel\McpSecurity\ApiKey\Storage\PdoStorage;
132+
133+
$pdo = new PDO('mysql:host=localhost;dbname=app', 'user', 'pass');
134+
$storage = new PdoStorage($pdo, 'mcp_api_keys');
135+
$storage->ensureTable(); // Creates table if needed
136+
```
137+
138+
### In-Memory (Testing)
139+
140+
```php
141+
use CodeWheel\McpSecurity\ApiKey\Storage\ArrayStorage;
142+
143+
$storage = new ArrayStorage();
144+
```
145+
146+
### Custom Storage
147+
148+
Implement `StorageInterface`:
149+
150+
```php
151+
use CodeWheel\McpSecurity\ApiKey\Storage\StorageInterface;
152+
153+
class RedisStorage implements StorageInterface
154+
{
155+
public function getAll(): array { /* ... */ }
156+
public function setAll(array $keys): void { /* ... */ }
157+
public function get(string $keyId): ?array { /* ... */ }
158+
public function set(string $keyId, array $data): void { /* ... */ }
159+
public function delete(string $keyId): bool { /* ... */ }
160+
}
161+
```
162+
163+
## Request Validation
164+
165+
### IP Allowlisting
166+
167+
```php
168+
use CodeWheel\McpSecurity\Validation\IpValidator;
169+
170+
$validator = new IpValidator([
171+
'127.0.0.1', // Single IP
172+
'10.0.0.0/8', // CIDR range
173+
'192.168.0.0/16', // Private network
174+
'::1', // IPv6 localhost
175+
]);
176+
177+
$validator->isAllowed('10.5.3.2'); // true
178+
$validator->isAllowed('8.8.8.8'); // false
179+
```
180+
181+
### Origin Allowlisting
182+
183+
```php
184+
use CodeWheel\McpSecurity\Validation\OriginValidator;
185+
186+
$validator = new OriginValidator([
187+
'localhost',
188+
'example.com',
189+
'*.example.com', // Wildcard: foo.example.com, bar.example.com
190+
]);
191+
192+
$validator->isAllowed('api.example.com'); // true
193+
$validator->isAllowed('evil.com'); // false
194+
```
195+
196+
### Combined Request Validation
197+
198+
```php
199+
use CodeWheel\McpSecurity\Validation\RequestValidator;
200+
201+
$validator = new RequestValidator(
202+
allowedIps: ['127.0.0.1', '10.0.0.0/8'],
203+
allowedOrigins: ['localhost', '*.myapp.com'],
204+
);
205+
206+
// With PSR-7 request
207+
$validator->validate($request); // Throws ValidationException if invalid
208+
$validator->isValid($request); // Returns bool
209+
```
210+
211+
## Middleware Configuration
212+
213+
```php
214+
use CodeWheel\McpSecurity\Config\SecurityConfig;
215+
216+
$config = new SecurityConfig(
217+
requireAuth: true, // Require API key
218+
allowedScopes: ['read'], // Only allow these scopes
219+
authHeader: 'Authorization', // Bearer token header
220+
apiKeyHeader: 'X-MCP-Api-Key', // Alternative header
221+
scopesAttribute: 'mcp.scopes', // Request attribute for scopes
222+
keyAttribute: 'mcp.key', // Request attribute for key info
223+
silentFail: true, // Return 404 instead of 401/403
224+
);
225+
```
226+
227+
## Error Handling
228+
229+
The middleware throws typed exceptions:
230+
231+
```php
232+
use CodeWheel\McpSecurity\Exception\AuthenticationException;
233+
use CodeWheel\McpSecurity\Exception\AuthorizationException;
234+
use CodeWheel\McpSecurity\Exception\RateLimitException;
235+
use CodeWheel\McpSecurity\Exception\ValidationException;
236+
237+
try {
238+
$middleware->process($request, $handler);
239+
} catch (AuthenticationException $e) {
240+
// 401 - Invalid or missing API key
241+
} catch (AuthorizationException $e) {
242+
// 403 - Insufficient scopes
243+
echo "Required: " . implode(', ', $e->requiredScopes);
244+
echo "Actual: " . implode(', ', $e->actualScopes);
245+
} catch (ValidationException $e) {
246+
// 404 - IP/Origin not allowed
247+
} catch (RateLimitException $e) {
248+
// 429 - Rate limited
249+
echo "Retry after: {$e->retryAfterSeconds} seconds";
250+
}
251+
```
252+
253+
## Framework Integration
254+
255+
### Slim 4
256+
257+
```php
258+
$app->add($securityMiddleware);
259+
```
260+
261+
### Laravel
262+
263+
```php
264+
// In a service provider
265+
$this->app->singleton(SecurityMiddleware::class, function ($app) {
266+
return new SecurityMiddleware(/* ... */);
267+
});
268+
269+
// In Kernel.php
270+
protected $middleware = [
271+
\CodeWheel\McpSecurity\Middleware\SecurityMiddleware::class,
272+
];
273+
```
274+
275+
### Drupal
276+
277+
See [drupal/mcp_tools](https://www.drupal.org/project/mcp_tools) which uses this package.
278+
279+
## Security Considerations
280+
281+
1. **Pepper your hashes** - Always provide a pepper for API key hashing
282+
2. **Use HTTPS** - Never transmit API keys over unencrypted connections
283+
3. **Rotate keys** - Use TTL and rotate keys regularly
284+
4. **Least privilege** - Grant minimal scopes needed
285+
5. **Audit logging** - Log key usage for security monitoring
286+
287+
## License
288+
289+
MIT License - see [LICENSE](LICENSE) file.
290+
291+
## Credits
292+
293+
Extracted from [drupal/mcp_tools](https://www.drupal.org/project/mcp_tools) by [CodeWheel](https://github.com/code-wheel).

composer.json

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"name": "code-wheel/mcp-http-security",
3+
"description": "Secure HTTP transport wrapper for MCP servers with API key auth, IP/Origin allowlisting, and rate limiting",
4+
"type": "library",
5+
"license": "MIT",
6+
"keywords": [
7+
"mcp",
8+
"model-context-protocol",
9+
"security",
10+
"api-key",
11+
"authentication",
12+
"middleware"
13+
],
14+
"authors": [
15+
{
16+
"name": "CodeWheel",
17+
"homepage": "https://github.com/code-wheel"
18+
}
19+
],
20+
"require": {
21+
"php": ">=8.1",
22+
"psr/http-server-middleware": "^1.0",
23+
"psr/http-message": "^1.0 || ^2.0",
24+
"psr/clock": "^1.0",
25+
"psr/log": "^1.0 || ^2.0 || ^3.0"
26+
},
27+
"require-dev": {
28+
"phpunit/phpunit": "^10.0 || ^11.0",
29+
"phpstan/phpstan": "^1.10"
30+
},
31+
"autoload": {
32+
"psr-4": {
33+
"CodeWheel\\McpSecurity\\": "src/"
34+
}
35+
},
36+
"autoload-dev": {
37+
"psr-4": {
38+
"CodeWheel\\McpSecurity\\Tests\\": "tests/"
39+
}
40+
},
41+
"config": {
42+
"sort-packages": true
43+
},
44+
"minimum-stability": "stable"
45+
}

phpunit.xml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
4+
bootstrap="vendor/autoload.php"
5+
colors="true"
6+
cacheDirectory=".phpunit.cache"
7+
executionOrder="depends,defects"
8+
requireCoverageMetadata="false"
9+
beStrictAboutCoverageMetadata="false"
10+
beStrictAboutOutputDuringTests="true"
11+
failOnRisky="true"
12+
failOnWarning="true">
13+
<testsuites>
14+
<testsuite name="default">
15+
<directory>tests</directory>
16+
</testsuite>
17+
</testsuites>
18+
19+
<source>
20+
<include>
21+
<directory suffix=".php">src</directory>
22+
</include>
23+
</source>
24+
</phpunit>

0 commit comments

Comments
 (0)