promptforge-mcp 0.1.0__tar.gz

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.
@@ -0,0 +1,9 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(test:*)",
5
+ "Bash(python -m build:*)",
6
+ "Bash(pip install:*)"
7
+ ]
8
+ }
9
+ }
@@ -0,0 +1,36 @@
1
+ # Python bytecode
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Virtual environments
7
+ .venv/
8
+ venv/
9
+ env/
10
+ ENV/
11
+
12
+ # Environment files
13
+ .env
14
+ .env.*
15
+ !.env.example
16
+ !.env.sample
17
+
18
+ # Testing and tooling caches
19
+ .pytest_cache/
20
+ .mypy_cache/
21
+ .ruff_cache/
22
+ .cache/
23
+ .coverage
24
+ coverage.xml
25
+ htmlcov/
26
+ .tox/
27
+ .nox/
28
+
29
+ # Packaging artifacts
30
+ build/
31
+ dist/
32
+ *.egg-info/
33
+ .eggs/
34
+
35
+ # Logs
36
+ *.log
@@ -0,0 +1,20 @@
1
+ # Prompt Forge MCP server Dockerfile
2
+ FROM python:3.11-slim
3
+
4
+ ENV PYTHONDONTWRITEBYTECODE=1
5
+ ENV PYTHONUNBUFFERED=1
6
+
7
+ WORKDIR /app
8
+
9
+ COPY pyproject.toml README.md LICENSE ./
10
+ COPY promptforge_mcp ./promptforge_mcp
11
+
12
+ RUN pip install --no-cache-dir .
13
+
14
+ ENV MCP_TRANSPORT=http
15
+ ENV MCP_HOST=0.0.0.0
16
+ ENV MCP_PORT=8765
17
+
18
+ EXPOSE 8765
19
+
20
+ CMD ["python", "-m", "promptforge_mcp.server"]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Prompt Forge
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.
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: promptforge-mcp
3
+ Version: 0.1.0
4
+ Summary: Prompt Forge MCP server for secure LLM gateway access
5
+ Project-URL: Homepage, https://secureprompt.tech
6
+ Project-URL: Documentation, https://docs.secureprompt.tech
7
+ Project-URL: Repository, https://github.com/promptforge/promptforge-mcp
8
+ Project-URL: Issues, https://github.com/promptforge/promptforge-mcp/issues
9
+ Author-email: Prompt Forge <support@secureprompt.tech>
10
+ License: MIT License
11
+
12
+ Copyright (c) 2026 Prompt Forge
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENSE
32
+ Keywords: gateway,llm,mcp,promptforge,security
33
+ Classifier: Development Status :: 3 - Alpha
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.9
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Requires-Python: >=3.9
42
+ Requires-Dist: httpx>=0.27.0
43
+ Requires-Dist: mcp<1.0,>=0.9.1
44
+ Requires-Dist: uvicorn>=0.27.0
45
+ Provides-Extra: dev
46
+ Requires-Dist: mypy>=1.8.0; extra == 'dev'
47
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
48
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
49
+ Requires-Dist: ruff>=0.2.0; extra == 'dev'
50
+ Description-Content-Type: text/markdown
51
+
52
+ # Prompt Forge MCP Server
53
+
54
+ Python MCP server that exposes [Prompt Forge](https://secureprompt.tech) secure gateway tools over the [Model Context Protocol](https://modelcontextprotocol.io/).
55
+
56
+ ## Install
57
+
58
+ ```bash
59
+ pip install promptforge-mcp
60
+ ```
61
+
62
+ ## Quick Start
63
+
64
+ ### Claude Desktop
65
+
66
+ Add to your `claude_desktop_config.json`:
67
+
68
+ ```json
69
+ {
70
+ "mcpServers": {
71
+ "promptforge": {
72
+ "command": "promptforge-mcp",
73
+ "env": {
74
+ "PROMPTFORGE_API_KEY": "your-api-key",
75
+ "PROMPTFORGE_ORG_ID": "your-org-id",
76
+ "PROMPTFORGE_ADMIN_TOKEN": "your-admin-token"
77
+ }
78
+ }
79
+ }
80
+ }
81
+ ```
82
+
83
+ ### Claude Code
84
+
85
+ ```bash
86
+ claude mcp add promptforge -- promptforge-mcp
87
+ ```
88
+
89
+ Then set environment variables in your shell before launching.
90
+
91
+ ### Run directly (stdio)
92
+
93
+ ```bash
94
+ export PROMPTFORGE_API_KEY="your-api-key"
95
+ promptforge-mcp
96
+ ```
97
+
98
+ ### Run as HTTP/SSE server
99
+
100
+ ```bash
101
+ promptforge-mcp --http
102
+ # or
103
+ MCP_TRANSPORT=http MCP_PORT=8765 promptforge-mcp
104
+ ```
105
+
106
+ ## Environment Variables
107
+
108
+ | Variable | Required | Default | Description |
109
+ |---|---|---|---|
110
+ | `PROMPTFORGE_API_KEY` | Yes (for gateway) | — | API key for gateway requests |
111
+ | `PROMPTFORGE_ADMIN_TOKEN` | Yes (for admin tools) | — | Token for secure mode and audit tools |
112
+ | `PROMPTFORGE_ORG_ID` | Yes (for admin tools) | — | Organization ID |
113
+ | `PROMPTFORGE_API_BASE` | No | `https://api.secureprompt.tech` | API base URL |
114
+ | `PROMPTFORGE_GATEWAY_URL` | No | `{API_BASE}/v1/gateway/requests` | Gateway endpoint |
115
+ | `PROMPTFORGE_TIMEOUT` | No | `30` | Request timeout in seconds |
116
+ | `PROMPTFORGE_MCP_TOOLS_PATH` | No | — | Override path for tool definitions JSON |
117
+ | `MCP_RATE_LIMIT_PER_MINUTE` | No | `60` | Rate limit per minute |
118
+ | `MCP_TRANSPORT` | No | `stdio` | Transport mode (`stdio` or `http`) |
119
+ | `MCP_HOST` | No | `0.0.0.0` | SSE server host |
120
+ | `MCP_PORT` | No | `8765` | SSE server port |
121
+
122
+ ## MCP Tools
123
+
124
+ | Tool | Description |
125
+ |---|---|
126
+ | `promptforge.secure_generate` | Send a request through the secure LLM gateway |
127
+ | `promptforge.redact_and_tokenize` | Tokenize sensitive entities using Secure Mode |
128
+ | `promptforge.policy_dry_run` | Evaluate an egress policy without executing a call |
129
+ | `promptforge.audit_query` | Query audit logs (admin-only) |
130
+ | `promptforge.detokenize` | Detokenize content with a token vault reference |
131
+
132
+ ## Docker
133
+
134
+ ```bash
135
+ docker build -t promptforge-mcp .
136
+ docker run -p 8765:8765 \
137
+ -e PROMPTFORGE_API_KEY=your-key \
138
+ -e PROMPTFORGE_ORG_ID=your-org \
139
+ -e PROMPTFORGE_ADMIN_TOKEN=your-token \
140
+ promptforge-mcp
141
+ ```
142
+
143
+ ## License
144
+
145
+ MIT
@@ -0,0 +1,94 @@
1
+ # Prompt Forge MCP Server
2
+
3
+ Python MCP server that exposes [Prompt Forge](https://secureprompt.tech) secure gateway tools over the [Model Context Protocol](https://modelcontextprotocol.io/).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install promptforge-mcp
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ### Claude Desktop
14
+
15
+ Add to your `claude_desktop_config.json`:
16
+
17
+ ```json
18
+ {
19
+ "mcpServers": {
20
+ "promptforge": {
21
+ "command": "promptforge-mcp",
22
+ "env": {
23
+ "PROMPTFORGE_API_KEY": "your-api-key",
24
+ "PROMPTFORGE_ORG_ID": "your-org-id",
25
+ "PROMPTFORGE_ADMIN_TOKEN": "your-admin-token"
26
+ }
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ ### Claude Code
33
+
34
+ ```bash
35
+ claude mcp add promptforge -- promptforge-mcp
36
+ ```
37
+
38
+ Then set environment variables in your shell before launching.
39
+
40
+ ### Run directly (stdio)
41
+
42
+ ```bash
43
+ export PROMPTFORGE_API_KEY="your-api-key"
44
+ promptforge-mcp
45
+ ```
46
+
47
+ ### Run as HTTP/SSE server
48
+
49
+ ```bash
50
+ promptforge-mcp --http
51
+ # or
52
+ MCP_TRANSPORT=http MCP_PORT=8765 promptforge-mcp
53
+ ```
54
+
55
+ ## Environment Variables
56
+
57
+ | Variable | Required | Default | Description |
58
+ |---|---|---|---|
59
+ | `PROMPTFORGE_API_KEY` | Yes (for gateway) | — | API key for gateway requests |
60
+ | `PROMPTFORGE_ADMIN_TOKEN` | Yes (for admin tools) | — | Token for secure mode and audit tools |
61
+ | `PROMPTFORGE_ORG_ID` | Yes (for admin tools) | — | Organization ID |
62
+ | `PROMPTFORGE_API_BASE` | No | `https://api.secureprompt.tech` | API base URL |
63
+ | `PROMPTFORGE_GATEWAY_URL` | No | `{API_BASE}/v1/gateway/requests` | Gateway endpoint |
64
+ | `PROMPTFORGE_TIMEOUT` | No | `30` | Request timeout in seconds |
65
+ | `PROMPTFORGE_MCP_TOOLS_PATH` | No | — | Override path for tool definitions JSON |
66
+ | `MCP_RATE_LIMIT_PER_MINUTE` | No | `60` | Rate limit per minute |
67
+ | `MCP_TRANSPORT` | No | `stdio` | Transport mode (`stdio` or `http`) |
68
+ | `MCP_HOST` | No | `0.0.0.0` | SSE server host |
69
+ | `MCP_PORT` | No | `8765` | SSE server port |
70
+
71
+ ## MCP Tools
72
+
73
+ | Tool | Description |
74
+ |---|---|
75
+ | `promptforge.secure_generate` | Send a request through the secure LLM gateway |
76
+ | `promptforge.redact_and_tokenize` | Tokenize sensitive entities using Secure Mode |
77
+ | `promptforge.policy_dry_run` | Evaluate an egress policy without executing a call |
78
+ | `promptforge.audit_query` | Query audit logs (admin-only) |
79
+ | `promptforge.detokenize` | Detokenize content with a token vault reference |
80
+
81
+ ## Docker
82
+
83
+ ```bash
84
+ docker build -t promptforge-mcp .
85
+ docker run -p 8765:8765 \
86
+ -e PROMPTFORGE_API_KEY=your-key \
87
+ -e PROMPTFORGE_ORG_ID=your-org \
88
+ -e PROMPTFORGE_ADMIN_TOKEN=your-token \
89
+ promptforge-mcp
90
+ ```
91
+
92
+ ## License
93
+
94
+ MIT
@@ -0,0 +1,10 @@
1
+ """Prompt Forge MCP server package."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ def main():
6
+ """Entry point for the MCP server."""
7
+ from promptforge_mcp.server import main as _main
8
+ _main()
9
+
10
+ __all__ = ["main", "__version__"]
File without changes
@@ -0,0 +1,245 @@
1
+ """Prompt Forge MCP server (stdio)."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import json
6
+ import os
7
+ import sys
8
+ import time
9
+ from collections import defaultdict
10
+ from dataclasses import dataclass
11
+ from typing import Any
12
+ from urllib.parse import urlencode
13
+
14
+ import httpx
15
+ import mcp.server.stdio
16
+ import mcp.types as types
17
+ from mcp.server import Server
18
+
19
+ from promptforge_mcp.tools import load_tools
20
+
21
+
22
+ # Rate limiting
23
+ class RateLimiter:
24
+ def __init__(self, max_requests_per_minute: int = 60):
25
+ self.max_requests = max_requests_per_minute
26
+ self.requests = defaultdict(list)
27
+
28
+ def check_rate_limit(self, client_id: str = "default") -> bool:
29
+ """Check if client is within rate limit. Returns True if allowed."""
30
+ now = time.time()
31
+ minute_ago = now - 60
32
+
33
+ # Clean old requests
34
+ self.requests[client_id] = [
35
+ req_time for req_time in self.requests[client_id]
36
+ if req_time > minute_ago
37
+ ]
38
+
39
+ # Check limit
40
+ if len(self.requests[client_id]) >= self.max_requests:
41
+ return False
42
+
43
+ # Record this request
44
+ self.requests[client_id].append(now)
45
+ return True
46
+
47
+
48
+ rate_limiter = RateLimiter(
49
+ max_requests_per_minute=int(os.getenv("MCP_RATE_LIMIT_PER_MINUTE", "60"))
50
+ )
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class MCPConfig:
55
+ api_base: str
56
+ gateway_url: str
57
+ api_key: str | None
58
+ admin_token: str | None
59
+ org_id: str | None
60
+ timeout: float
61
+
62
+ @staticmethod
63
+ def from_env() -> "MCPConfig":
64
+ api_base = os.getenv("PROMPTFORGE_API_BASE", "https://api.secureprompt.tech")
65
+ gateway_url = os.getenv("PROMPTFORGE_GATEWAY_URL", f"{api_base}/v1/gateway/requests")
66
+ return MCPConfig(
67
+ api_base=api_base,
68
+ gateway_url=gateway_url,
69
+ api_key=os.getenv("PROMPTFORGE_API_KEY"),
70
+ admin_token=os.getenv("PROMPTFORGE_ADMIN_TOKEN"),
71
+ org_id=os.getenv("PROMPTFORGE_ORG_ID"),
72
+ timeout=float(os.getenv("PROMPTFORGE_TIMEOUT", "30")),
73
+ )
74
+
75
+
76
+ TOOLS = load_tools()
77
+ TOOLS_BY_NAME = {tool["name"]: tool for tool in TOOLS}
78
+
79
+ server = Server("promptforge-mcp")
80
+
81
+
82
+ @server.list_tools()
83
+ async def list_tools() -> list[types.Tool]:
84
+ return [
85
+ types.Tool(
86
+ name=tool["name"],
87
+ description=tool.get("description", ""),
88
+ inputSchema=tool.get("input_schema") or tool.get("inputSchema") or {},
89
+ )
90
+ for tool in TOOLS
91
+ ]
92
+
93
+
94
+ async def _request_json(
95
+ method: str,
96
+ url: str,
97
+ *,
98
+ headers: dict[str, str] | None = None,
99
+ payload: dict[str, Any] | None = None,
100
+ timeout: float = 30,
101
+ ) -> dict[str, Any]:
102
+ async with httpx.AsyncClient(timeout=timeout) as client:
103
+ response = await client.request(method, url, json=payload, headers=headers)
104
+ if response.status_code >= 400:
105
+ raise ValueError(f"API request failed (HTTP {response.status_code})")
106
+ return response.json()
107
+
108
+
109
+ def _resolve_org_id(config: MCPConfig, arguments: dict[str, Any]) -> str | None:
110
+ return arguments.get("org_id") or config.org_id
111
+
112
+
113
+ @server.call_tool()
114
+ async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.TextContent]:
115
+ # Rate limiting check
116
+ if not rate_limiter.check_rate_limit():
117
+ raise ValueError("Rate limit exceeded. Please try again later.")
118
+
119
+ config = MCPConfig.from_env()
120
+ tool = TOOLS_BY_NAME.get(name)
121
+ if not tool:
122
+ raise ValueError(f"Unknown tool: {name}")
123
+
124
+ if name == "promptforge.secure_generate":
125
+ if not config.api_key:
126
+ raise ValueError("PROMPTFORGE_API_KEY is required")
127
+ headers = {"X-API-Key": config.api_key}
128
+ result = await _request_json(
129
+ "POST",
130
+ config.gateway_url,
131
+ headers=headers,
132
+ payload=arguments,
133
+ timeout=config.timeout,
134
+ )
135
+ return [types.TextContent(type="text", text=json.dumps(result))]
136
+
137
+ if name == "promptforge.redact_and_tokenize":
138
+ org_id = _resolve_org_id(config, arguments)
139
+ if not config.admin_token or not org_id:
140
+ raise ValueError("PROMPTFORGE_ADMIN_TOKEN and PROMPTFORGE_ORG_ID are required")
141
+ payload = dict(arguments)
142
+ payload.pop("org_id", None)
143
+ headers = {"Authorization": f"Bearer {config.admin_token}"}
144
+ url = f"{config.api_base}/v1/secure-mode/tokenize?org_id={org_id}"
145
+ result = await _request_json("POST", url, headers=headers, payload=payload, timeout=config.timeout)
146
+ return [types.TextContent(type="text", text=json.dumps(result))]
147
+
148
+ if name == "promptforge.policy_dry_run":
149
+ org_id = _resolve_org_id(config, arguments)
150
+ if not config.admin_token or not org_id:
151
+ raise ValueError("PROMPTFORGE_ADMIN_TOKEN and PROMPTFORGE_ORG_ID are required")
152
+ payload = dict(arguments)
153
+ payload.pop("org_id", None)
154
+ headers = {"Authorization": f"Bearer {config.admin_token}"}
155
+ url = f"{config.api_base}/v1/policies/dry-run?org_id={org_id}"
156
+ result = await _request_json("POST", url, headers=headers, payload=payload, timeout=config.timeout)
157
+ return [types.TextContent(type="text", text=json.dumps(result))]
158
+
159
+ if name == "promptforge.audit_query":
160
+ org_id = _resolve_org_id(config, arguments)
161
+ if not config.admin_token or not org_id:
162
+ raise ValueError("PROMPTFORGE_ADMIN_TOKEN and PROMPTFORGE_ORG_ID are required")
163
+ params = {"org_id": org_id}
164
+ for key in ["start_date", "end_date", "action", "user_id", "limit", "cursor"]:
165
+ if key in arguments and arguments[key] is not None:
166
+ params[key] = arguments[key]
167
+ headers = {"Authorization": f"Bearer {config.admin_token}"}
168
+ url = f"{config.api_base}/v1/audit-logs?{urlencode(params)}"
169
+ result = await _request_json("GET", url, headers=headers, timeout=config.timeout)
170
+ return [types.TextContent(type="text", text=json.dumps(result))]
171
+
172
+ if name == "promptforge.detokenize":
173
+ org_id = _resolve_org_id(config, arguments)
174
+ if not config.admin_token or not org_id:
175
+ raise ValueError("PROMPTFORGE_ADMIN_TOKEN and PROMPTFORGE_ORG_ID are required")
176
+ payload = dict(arguments)
177
+ payload.pop("org_id", None)
178
+ headers = {"Authorization": f"Bearer {config.admin_token}"}
179
+ url = f"{config.api_base}/v1/secure-mode/detokenize?org_id={org_id}"
180
+ result = await _request_json("POST", url, headers=headers, payload=payload, timeout=config.timeout)
181
+ return [types.TextContent(type="text", text=json.dumps(result))]
182
+
183
+ raise ValueError(f"Unhandled tool: {name}")
184
+
185
+
186
+ async def run_stdio() -> None:
187
+ async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
188
+ await server.run(
189
+ read_stream,
190
+ write_stream,
191
+ server.create_initialization_options(),
192
+ )
193
+
194
+
195
+ async def run_sse(host: str = "0.0.0.0", port: int = 8765) -> None:
196
+ import uvicorn
197
+ from mcp.server.sse import SseServerTransport
198
+
199
+ transport = SseServerTransport("/messages")
200
+
201
+ async def app(scope, receive, send):
202
+ if scope["type"] != "http":
203
+ return
204
+ path = scope.get("path", "")
205
+ if path == "/sse":
206
+ async with transport.connect_sse(scope, receive, send) as (read_stream, write_stream):
207
+ await server.run(
208
+ read_stream,
209
+ write_stream,
210
+ server.create_initialization_options(),
211
+ )
212
+ elif path == "/messages":
213
+ await transport.handle_post_message(scope, receive, send)
214
+ else:
215
+ await send({"type": "http.response.start", "status": 404, "headers": [(b"content-type", b"text/plain")]})
216
+ await send({"type": "http.response.body", "body": b"Not Found"})
217
+
218
+ max_connections = int(os.getenv("MCP_MAX_CONNECTIONS", "100"))
219
+ config = uvicorn.Config(
220
+ app,
221
+ host=host,
222
+ port=port,
223
+ log_level="info",
224
+ limit_concurrency=max_connections,
225
+ timeout_keep_alive=30,
226
+ )
227
+ srv = uvicorn.Server(config)
228
+ await srv.serve()
229
+
230
+
231
+ def main() -> None:
232
+ mode = os.getenv("MCP_TRANSPORT", "stdio")
233
+ if "--http" in sys.argv:
234
+ mode = "http"
235
+ host = os.getenv("MCP_HOST", "0.0.0.0")
236
+ port = int(os.getenv("MCP_PORT", "8765"))
237
+
238
+ if mode == "http":
239
+ asyncio.run(run_sse(host=host, port=port))
240
+ else:
241
+ asyncio.run(run_stdio())
242
+
243
+
244
+ if __name__ == "__main__":
245
+ main()
@@ -0,0 +1,168 @@
1
+ """MCP tool definitions for Prompt Forge."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+
8
+ DEFAULT_TOOLS = [
9
+ {
10
+ "name": "promptforge.secure_generate",
11
+ "description": "Send a request through the Prompt Forge secure gateway.",
12
+ "input_schema": {
13
+ "type": "object",
14
+ "required": ["provider", "model", "messages"],
15
+ "properties": {
16
+ "provider": {"type": "string"},
17
+ "model": {"type": "string"},
18
+ "messages": {
19
+ "type": "array",
20
+ "items": {
21
+ "type": "object",
22
+ "required": ["role", "content"],
23
+ "properties": {
24
+ "role": {"type": "string"},
25
+ "content": {"type": "string"},
26
+ },
27
+ },
28
+ },
29
+ "temperature": {"type": "number"},
30
+ "top_p": {"type": "number"},
31
+ "max_tokens": {"type": "integer"},
32
+ "stream": {"type": "boolean"},
33
+ "tools": {"type": "array", "items": {"type": "object"}},
34
+ "tool_choice": {"oneOf": [{"type": "string"}, {"type": "object"}]},
35
+ "user": {"type": "string"},
36
+ "use_case": {"type": "string"},
37
+ "metadata": {"type": "object"},
38
+ "trace_id": {"type": "string", "format": "uuid"},
39
+ "policy_id": {"type": "string", "format": "uuid"},
40
+ },
41
+ },
42
+ "output_schema": {
43
+ "type": "object",
44
+ "required": ["id", "provider", "model"],
45
+ "properties": {
46
+ "id": {"type": "string", "format": "uuid"},
47
+ "provider": {"type": "string"},
48
+ "model": {"type": "string"},
49
+ "output_text": {"type": "string"},
50
+ "choices": {"type": "array", "items": {"type": "object"}},
51
+ "usage": {"type": "object"},
52
+ "latency_ms": {"type": "integer"},
53
+ "cost_usd": {"type": "number"},
54
+ "security": {"type": "object"},
55
+ "trace_id": {"type": "string", "format": "uuid"},
56
+ "request_id": {"type": "string"},
57
+ },
58
+ },
59
+ },
60
+ {
61
+ "name": "promptforge.redact_and_tokenize",
62
+ "description": "Tokenize sensitive entities using Secure Mode.",
63
+ "input_schema": {
64
+ "type": "object",
65
+ "required": ["text"],
66
+ "properties": {
67
+ "text": {"type": "string"},
68
+ "entity_types": {"type": "array", "items": {"type": "string"}},
69
+ "fail_closed": {"type": "boolean"},
70
+ "metadata": {"type": "object"},
71
+ },
72
+ },
73
+ "output_schema": {
74
+ "type": "object",
75
+ "required": ["tokenized_text", "token_vault_id", "entity_counts"],
76
+ "properties": {
77
+ "tokenized_text": {"type": "string"},
78
+ "token_vault_id": {"type": "string", "format": "uuid"},
79
+ "entity_counts": {"type": "object"},
80
+ },
81
+ },
82
+ },
83
+ {
84
+ "name": "promptforge.policy_dry_run",
85
+ "description": "Evaluate an egress policy decision without executing a provider call.",
86
+ "input_schema": {
87
+ "type": "object",
88
+ "required": ["provider", "model"],
89
+ "properties": {
90
+ "provider": {"type": "string"},
91
+ "model": {"type": "string"},
92
+ "region": {"type": "string"},
93
+ "policy_id": {"type": "string", "format": "uuid"},
94
+ },
95
+ },
96
+ "output_schema": {
97
+ "type": "object",
98
+ "required": ["allowed", "fail_closed"],
99
+ "properties": {
100
+ "allowed": {"type": "boolean"},
101
+ "reason": {"type": "string"},
102
+ "policy_id": {"type": "string", "format": "uuid"},
103
+ "rule_ids": {"type": "array", "items": {"type": "string"}},
104
+ "fail_closed": {"type": "boolean"},
105
+ "retention_days": {"type": "integer"},
106
+ },
107
+ },
108
+ },
109
+ {
110
+ "name": "promptforge.audit_query",
111
+ "description": "Query audit logs (admin-only).",
112
+ "input_schema": {
113
+ "type": "object",
114
+ "properties": {
115
+ "org_id": {"type": "string", "format": "uuid"},
116
+ "start_date": {"type": "string", "format": "date-time"},
117
+ "end_date": {"type": "string", "format": "date-time"},
118
+ "action": {"type": "string"},
119
+ "user_id": {"type": "string", "format": "uuid"},
120
+ "limit": {"type": "integer"},
121
+ "cursor": {"type": "string"},
122
+ },
123
+ },
124
+ "output_schema": {
125
+ "type": "object",
126
+ "properties": {
127
+ "logs": {"type": "array", "items": {"type": "object"}},
128
+ "next_cursor": {"type": "string"},
129
+ },
130
+ },
131
+ },
132
+ {
133
+ "name": "promptforge.detokenize",
134
+ "description": "Detokenize content with an authorized token vault reference.",
135
+ "input_schema": {
136
+ "type": "object",
137
+ "required": ["token_vault_id", "text"],
138
+ "properties": {
139
+ "token_vault_id": {"type": "string", "format": "uuid"},
140
+ "text": {"type": "string"},
141
+ },
142
+ },
143
+ "output_schema": {
144
+ "type": "object",
145
+ "required": ["text"],
146
+ "properties": {
147
+ "text": {"type": "string"},
148
+ },
149
+ },
150
+ },
151
+ ]
152
+
153
+
154
+ def load_tools() -> list[dict]:
155
+ """Load tool definitions from disk if available."""
156
+ override = os.getenv("PROMPTFORGE_MCP_TOOLS_PATH")
157
+ if override and Path(override).exists():
158
+ with open(override, "r", encoding="utf-8") as handle:
159
+ data = json.load(handle)
160
+ return data.get("tools", DEFAULT_TOOLS)
161
+
162
+ candidate = Path(__file__).resolve().parents[2] / "promptforge-schemas" / "mcp" / "tools.v1.json"
163
+ if candidate.exists():
164
+ with open(candidate, "r", encoding="utf-8") as handle:
165
+ data = json.load(handle)
166
+ return data.get("tools", DEFAULT_TOOLS)
167
+
168
+ return DEFAULT_TOOLS
@@ -0,0 +1,62 @@
1
+ [project]
2
+ name = "promptforge-mcp"
3
+ version = "0.1.0"
4
+ description = "Prompt Forge MCP server for secure LLM gateway access"
5
+ readme = {file = "README.md", content-type = "text/markdown"}
6
+ license = { file = "LICENSE" }
7
+ requires-python = ">=3.9"
8
+ authors = [
9
+ { name = "Prompt Forge", email = "support@secureprompt.tech" }
10
+ ]
11
+ keywords = ["mcp", "llm", "security", "gateway", "promptforge"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.9",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ ]
22
+ dependencies = [
23
+ "httpx>=0.27.0",
24
+ "mcp>=0.9.1,<1.0",
25
+ "uvicorn>=0.27.0",
26
+ ]
27
+
28
+ [project.optional-dependencies]
29
+ dev = [
30
+ "pytest>=8.0.0",
31
+ "pytest-asyncio>=0.23.0",
32
+ "ruff>=0.2.0",
33
+ "mypy>=1.8.0",
34
+ ]
35
+
36
+ [project.scripts]
37
+ promptforge-mcp = "promptforge_mcp:main"
38
+
39
+ [project.urls]
40
+ Homepage = "https://secureprompt.tech"
41
+ Documentation = "https://docs.secureprompt.tech"
42
+ Repository = "https://github.com/promptforge/promptforge-mcp"
43
+ Issues = "https://github.com/promptforge/promptforge-mcp/issues"
44
+
45
+ [build-system]
46
+ requires = ["hatchling"]
47
+ build-backend = "hatchling.build"
48
+
49
+ [tool.hatch.build.targets.wheel]
50
+ packages = ["promptforge_mcp"]
51
+
52
+ [tool.pytest.ini_options]
53
+ asyncio_mode = "auto"
54
+ testpaths = ["tests"]
55
+
56
+ [tool.ruff]
57
+ line-length = 100
58
+ target-version = "py39"
59
+
60
+ [tool.mypy]
61
+ python_version = "3.9"
62
+ strict = true