promptforge-mcp 0.1.0__py3-none-any.whl
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.
- promptforge_mcp/__init__.py +10 -0
- promptforge_mcp/py.typed +0 -0
- promptforge_mcp/server.py +245 -0
- promptforge_mcp/tools.py +168 -0
- promptforge_mcp-0.1.0.dist-info/METADATA +145 -0
- promptforge_mcp-0.1.0.dist-info/RECORD +9 -0
- promptforge_mcp-0.1.0.dist-info/WHEEL +4 -0
- promptforge_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- promptforge_mcp-0.1.0.dist-info/licenses/LICENSE +21 -0
promptforge_mcp/py.typed
ADDED
|
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()
|
promptforge_mcp/tools.py
ADDED
|
@@ -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,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,9 @@
|
|
|
1
|
+
promptforge_mcp/__init__.py,sha256=fS7ld2cam_u6E9DUTE2yLe-65X6gAB4PGPCMfGGuNzg,217
|
|
2
|
+
promptforge_mcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
promptforge_mcp/server.py,sha256=SUDe-N3Ew9KMoYgNO16KE1wrR1Q4aexMybrjzov21j0,8520
|
|
4
|
+
promptforge_mcp/tools.py,sha256=r7QsNdOyGZxPq62MtTadBoxtc7cQ5FK1ESVLtJoe4yc,6374
|
|
5
|
+
promptforge_mcp-0.1.0.dist-info/METADATA,sha256=1Zmd--8aHtvabW5fm4sYz_2VphNBzRupCrXxowkUnXk,5021
|
|
6
|
+
promptforge_mcp-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
7
|
+
promptforge_mcp-0.1.0.dist-info/entry_points.txt,sha256=vrPYJd8B77DgWRMhrfhbgd7Jlq4HUR7A8packKjJxBs,57
|
|
8
|
+
promptforge_mcp-0.1.0.dist-info/licenses/LICENSE,sha256=BXBSztG1vwY2paN122S9pUIYsk-HdL_iGivPlIIeQ9w,1069
|
|
9
|
+
promptforge_mcp-0.1.0.dist-info/RECORD,,
|
|
@@ -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.
|