@prompt-ot/mcp 0.1.0
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/README.md +79 -0
- package/dist/client.js +89 -0
- package/dist/client.js.map +1 -0
- package/dist/http.js +189 -0
- package/dist/http.js.map +1 -0
- package/dist/lib/env.js +34 -0
- package/dist/lib/env.js.map +1 -0
- package/dist/lib/errors.js +36 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/format.js +44 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/resources/prompts.js +129 -0
- package/dist/resources/prompts.js.map +1 -0
- package/dist/server.js +42 -0
- package/dist/server.js.map +1 -0
- package/dist/stdio.js +30 -0
- package/dist/stdio.js.map +1 -0
- package/dist/tools/blocks.js +156 -0
- package/dist/tools/blocks.js.map +1 -0
- package/dist/tools/prompts.js +201 -0
- package/dist/tools/prompts.js.map +1 -0
- package/dist/tools/test-cases.js +165 -0
- package/dist/tools/test-cases.js.map +1 -0
- package/dist/tools/variables.js +103 -0
- package/dist/tools/variables.js.map +1 -0
- package/dist/tools/versions.js +197 -0
- package/dist/tools/versions.js.map +1 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# @prompt-ot/mcp
|
|
2
|
+
|
|
3
|
+
Model Context Protocol server for [PromptOT](https://www.promptot.com).
|
|
4
|
+
|
|
5
|
+
Use PromptOT directly from Claude Desktop, Cursor, Codex CLI, ChatGPT, claude.ai, or any other MCP-compatible AI tool. The server exposes ~35 tools for managing prompts, blocks, variables, versions, test cases, and more — all gated by scoped API keys.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
### Claude Desktop
|
|
10
|
+
|
|
11
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"mcpServers": {
|
|
16
|
+
"promptot": {
|
|
17
|
+
"command": "npx",
|
|
18
|
+
"args": ["-y", "@prompt-ot/mcp"],
|
|
19
|
+
"env": {
|
|
20
|
+
"PROMPTOT_API_KEY": "pot_...",
|
|
21
|
+
"PROMPTOT_MCP_CLIENT": "claude-desktop"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Generate your API key from the PromptOT dashboard → project → API Keys → "Generate MCP Key".
|
|
29
|
+
|
|
30
|
+
### Cursor
|
|
31
|
+
|
|
32
|
+
Add to `~/.cursor/mcp.json` (or `.cursor/mcp.json` in your workspace root):
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"mcpServers": {
|
|
37
|
+
"promptot": {
|
|
38
|
+
"command": "npx",
|
|
39
|
+
"args": ["-y", "@prompt-ot/mcp"],
|
|
40
|
+
"env": {
|
|
41
|
+
"PROMPTOT_API_KEY": "pot_...",
|
|
42
|
+
"PROMPTOT_MCP_CLIENT": "cursor"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Codex CLI
|
|
50
|
+
|
|
51
|
+
Add to `~/.codex/config.toml`:
|
|
52
|
+
|
|
53
|
+
```toml
|
|
54
|
+
[mcp_servers.promptot]
|
|
55
|
+
command = "npx"
|
|
56
|
+
args = ["-y", "@prompt-ot/mcp"]
|
|
57
|
+
env = { PROMPTOT_API_KEY = "pot_...", PROMPTOT_MCP_CLIENT = "codex-cli" }
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### ChatGPT & claude.ai (hosted)
|
|
61
|
+
|
|
62
|
+
The hosted transport is at `https://mcp.promptot.com/mcp`. Connect via OAuth from the client's MCP settings — no config file needed.
|
|
63
|
+
|
|
64
|
+
## Environment variables
|
|
65
|
+
|
|
66
|
+
| Variable | Required | Default | Description |
|
|
67
|
+
|---|---|---|---|
|
|
68
|
+
| `PROMPTOT_API_KEY` | Yes | — | Your scoped API key from the PromptOT dashboard |
|
|
69
|
+
| `PROMPTOT_API_URL` | No | `https://api.promptot.com` | Override for self-hosted or local dev |
|
|
70
|
+
| `PROMPTOT_MCP_CLIENT` | No | `unknown` | Client identifier for analytics (`claude-desktop`, `cursor`, `codex-cli`, etc.) |
|
|
71
|
+
| `PROMPTOT_DEBUG` | No | `false` | Enable verbose stderr logging for troubleshooting |
|
|
72
|
+
|
|
73
|
+
## Docs
|
|
74
|
+
|
|
75
|
+
Full tool reference and FAQ: https://www.promptot.com/docs/mcp
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { PromptOTError } from './lib/errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Thin fetch wrapper around the PromptOT HTTP API.
|
|
4
|
+
*
|
|
5
|
+
* Every request carries:
|
|
6
|
+
* - Authorization: Bearer <API key>
|
|
7
|
+
* - X-PromptOT-Client: mcp (so the API can tag request_logs.source='mcp')
|
|
8
|
+
* - X-PromptOT-Client-Name: <client identifier> (claude-desktop, cursor, …)
|
|
9
|
+
* - X-PromptOT-Tool: <tool name> (so audit logs can attribute the action)
|
|
10
|
+
*
|
|
11
|
+
* Errors in the API envelope are unwrapped into PromptOTError instances so
|
|
12
|
+
* tool handlers can surface them cleanly to the LLM.
|
|
13
|
+
*/
|
|
14
|
+
export class PromptOTClient {
|
|
15
|
+
env;
|
|
16
|
+
constructor(env) {
|
|
17
|
+
this.env = env;
|
|
18
|
+
}
|
|
19
|
+
async get(path, opts = {}) {
|
|
20
|
+
return this.request('GET', path, undefined, opts);
|
|
21
|
+
}
|
|
22
|
+
async post(path, body, opts = {}) {
|
|
23
|
+
return this.request('POST', path, body, opts);
|
|
24
|
+
}
|
|
25
|
+
async patch(path, body, opts = {}) {
|
|
26
|
+
return this.request('PATCH', path, body, opts);
|
|
27
|
+
}
|
|
28
|
+
async delete(path, opts = {}) {
|
|
29
|
+
return this.request('DELETE', path, undefined, opts);
|
|
30
|
+
}
|
|
31
|
+
async request(method, path, body, opts) {
|
|
32
|
+
const url = new URL(path.startsWith('http') ? path : `${this.env.apiUrl}${path}`);
|
|
33
|
+
if (opts.query) {
|
|
34
|
+
for (const [key, value] of Object.entries(opts.query)) {
|
|
35
|
+
if (value !== undefined) {
|
|
36
|
+
url.searchParams.set(key, String(value));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const headers = {
|
|
41
|
+
'Authorization': `Bearer ${this.env.apiKey}`,
|
|
42
|
+
'X-PromptOT-Client': 'mcp',
|
|
43
|
+
'X-PromptOT-Client-Name': this.env.clientName,
|
|
44
|
+
'User-Agent': `@prompt-ot/mcp (${this.env.clientName})`,
|
|
45
|
+
};
|
|
46
|
+
if (opts.tool) {
|
|
47
|
+
headers['X-PromptOT-Tool'] = opts.tool;
|
|
48
|
+
}
|
|
49
|
+
if (body !== undefined) {
|
|
50
|
+
headers['Content-Type'] = 'application/json';
|
|
51
|
+
}
|
|
52
|
+
if (this.env.debug) {
|
|
53
|
+
process.stderr.write(`[promptot-mcp] ${method} ${url.pathname}${url.search}\n`);
|
|
54
|
+
}
|
|
55
|
+
const res = await fetch(url, {
|
|
56
|
+
method,
|
|
57
|
+
headers,
|
|
58
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
59
|
+
});
|
|
60
|
+
// Retry once on 429, honoring Retry-After
|
|
61
|
+
if (res.status === 429 && opts.retry !== false) {
|
|
62
|
+
const retryAfter = parseInt(res.headers.get('retry-after') || '1', 10);
|
|
63
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(retryAfter, 10) * 1000));
|
|
64
|
+
return this.request(method, path, body, { ...opts, retry: false });
|
|
65
|
+
}
|
|
66
|
+
const text = await res.text();
|
|
67
|
+
let envelope;
|
|
68
|
+
try {
|
|
69
|
+
envelope = text ? JSON.parse(text) : { data: null, error: null };
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
throw new PromptOTError(res.status, {
|
|
73
|
+
code: 'INVALID_RESPONSE',
|
|
74
|
+
message: `Non-JSON response from API (${res.status}): ${text.slice(0, 200)}`,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (envelope.error) {
|
|
78
|
+
throw new PromptOTError(res.status, envelope.error);
|
|
79
|
+
}
|
|
80
|
+
if (!res.ok) {
|
|
81
|
+
throw new PromptOTError(res.status, {
|
|
82
|
+
code: 'HTTP_ERROR',
|
|
83
|
+
message: `HTTP ${res.status} ${res.statusText}`,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return envelope.data;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Hosted HTTP entry point for @prompt-ot/mcp.
|
|
4
|
+
*
|
|
5
|
+
* Runs as a separate Railway service at https://mcp.promptot.com/mcp.
|
|
6
|
+
* Implements the MCP Streamable HTTP transport spec (POST /mcp + GET /mcp)
|
|
7
|
+
* and accepts the OAuth-issued access token directly as the API key —
|
|
8
|
+
* the token IS a PromptOT API key minted by /api/oauth/token. There is
|
|
9
|
+
* no per-request session lookup; this server is a thin proxy that takes
|
|
10
|
+
* the bearer token, builds a PromptOTClient, and runs a fresh stateless
|
|
11
|
+
* MCP server per request.
|
|
12
|
+
*
|
|
13
|
+
* Why stateless per-request? Cleanest scaling story for a hosted MCP:
|
|
14
|
+
* each tool call gets its own server instance, no shared state to
|
|
15
|
+
* coordinate across nodes, no in-memory session map to evict, trivial
|
|
16
|
+
* horizontal scaling. The tradeoff is a tiny per-request setup cost
|
|
17
|
+
* (creating an McpServer + StreamableHTTPServerTransport) which is
|
|
18
|
+
* negligible compared to the upstream HTTP call to apps/api anyway.
|
|
19
|
+
*
|
|
20
|
+
* Auth flow recap:
|
|
21
|
+
* 1. Client (claude.ai, ChatGPT) hits POST /mcp with no auth → 401
|
|
22
|
+
* with WWW-Authenticate header pointing at the OAuth metadata.
|
|
23
|
+
* 2. Client follows the OAuth flow ending with a call to
|
|
24
|
+
* POST {API_URL}/api/oauth/token which mints a real API key.
|
|
25
|
+
* 3. Client sends Authorization: Bearer pot_... on every /mcp request.
|
|
26
|
+
* 4. This server forwards that token to apps/api via PromptOTClient.
|
|
27
|
+
*/
|
|
28
|
+
import express from 'express';
|
|
29
|
+
import { randomUUID } from 'node:crypto';
|
|
30
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
31
|
+
import { PromptOTClient } from './client.js';
|
|
32
|
+
import { createMcpServer } from './server.js';
|
|
33
|
+
import { readEnv } from './lib/env.js';
|
|
34
|
+
const PORT = parseInt(process.env.PORT || '3002', 10);
|
|
35
|
+
const PUBLIC_API_URL = (process.env.PROMPTOT_API_URL || 'https://api.promptot.com').replace(/\/$/, '');
|
|
36
|
+
// CORS allowlist for the hosted MCP server. We're stricter than the main
|
|
37
|
+
// API because this surface only serves browser-based MCP clients — there's
|
|
38
|
+
// no server-to-server use case. Add new clients here as the MCP ecosystem
|
|
39
|
+
// grows.
|
|
40
|
+
const CORS_ALLOWLIST = [
|
|
41
|
+
/^https:\/\/claude\.ai$/,
|
|
42
|
+
/^https:\/\/.*\.claude\.ai$/,
|
|
43
|
+
/^https:\/\/chat\.openai\.com$/,
|
|
44
|
+
/^https:\/\/chatgpt\.com$/,
|
|
45
|
+
/^https:\/\/.*\.chatgpt\.com$/,
|
|
46
|
+
/^https:\/\/cursor\.com$/,
|
|
47
|
+
/^https:\/\/.*\.cursor\.sh$/,
|
|
48
|
+
// Local dev for Inspector + manual testing
|
|
49
|
+
/^http:\/\/localhost:\d+$/,
|
|
50
|
+
/^http:\/\/127\.0\.0\.1:\d+$/,
|
|
51
|
+
];
|
|
52
|
+
function corsMiddleware(req, res, next) {
|
|
53
|
+
const origin = req.headers.origin;
|
|
54
|
+
if (typeof origin === 'string' && CORS_ALLOWLIST.some((re) => re.test(origin))) {
|
|
55
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
56
|
+
res.setHeader('Vary', 'Origin');
|
|
57
|
+
}
|
|
58
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE');
|
|
59
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Mcp-Session-Id, Mcp-Protocol-Version');
|
|
60
|
+
res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
|
|
61
|
+
res.setHeader('Access-Control-Max-Age', '86400');
|
|
62
|
+
if (req.method === 'OPTIONS') {
|
|
63
|
+
res.status(204).end();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
next();
|
|
67
|
+
}
|
|
68
|
+
function extractBearer(req) {
|
|
69
|
+
const header = req.headers.authorization;
|
|
70
|
+
if (typeof header !== 'string' || !header.startsWith('Bearer '))
|
|
71
|
+
return null;
|
|
72
|
+
return header.slice(7).trim() || null;
|
|
73
|
+
}
|
|
74
|
+
function send401(res, message) {
|
|
75
|
+
// Per OAuth 2.0 / RFC 6750 + the MCP OAuth spec, we return a 401 with a
|
|
76
|
+
// WWW-Authenticate header pointing at the resource's OAuth metadata so
|
|
77
|
+
// discovery clients can find the authorize/token endpoints automatically.
|
|
78
|
+
res.setHeader('WWW-Authenticate', `Bearer realm="promptot", resource_metadata="${PUBLIC_API_URL}/api/oauth/.well-known/oauth-protected-resource"`);
|
|
79
|
+
res.status(401).json({
|
|
80
|
+
error: 'invalid_token',
|
|
81
|
+
error_description: message,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const app = express();
|
|
85
|
+
app.set('trust proxy', 1);
|
|
86
|
+
app.use(corsMiddleware);
|
|
87
|
+
app.use(express.json({ limit: '10mb' }));
|
|
88
|
+
// Health check — Railway uses this for the readiness probe
|
|
89
|
+
app.get('/health', (_req, res) => {
|
|
90
|
+
res.json({ status: 'ok', service: 'mcp-http', timestamp: new Date().toISOString() });
|
|
91
|
+
});
|
|
92
|
+
// Discovery endpoints — proxied through to the main API so MCP clients
|
|
93
|
+
// only need to know about mcp.promptot.com. The main API serves the
|
|
94
|
+
// canonical metadata at /api/oauth/.well-known/* so we re-export here.
|
|
95
|
+
app.get('/.well-known/oauth-protected-resource', (_req, res) => {
|
|
96
|
+
res.redirect(`${PUBLIC_API_URL}/api/oauth/.well-known/oauth-protected-resource`);
|
|
97
|
+
});
|
|
98
|
+
app.get('/.well-known/oauth-authorization-server', (_req, res) => {
|
|
99
|
+
res.redirect(`${PUBLIC_API_URL}/api/oauth/.well-known/oauth-authorization-server`);
|
|
100
|
+
});
|
|
101
|
+
// =============================================================================
|
|
102
|
+
// POST /mcp — JSON-RPC inbound
|
|
103
|
+
// =============================================================================
|
|
104
|
+
async function handleMcpRequest(req, res) {
|
|
105
|
+
const accessToken = extractBearer(req);
|
|
106
|
+
if (!accessToken) {
|
|
107
|
+
send401(res, 'Missing Authorization: Bearer header');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// The hosted server runs in stateless mode — every request gets a fresh
|
|
111
|
+
// McpServer + StreamableHTTPServerTransport pair. The transport hands
|
|
112
|
+
// back the JSON-RPC response (or starts an SSE stream) and we tear it
|
|
113
|
+
// down at the end of the request.
|
|
114
|
+
const baseEnv = readEnv();
|
|
115
|
+
const client = new PromptOTClient({
|
|
116
|
+
apiKey: accessToken,
|
|
117
|
+
apiUrl: baseEnv.apiUrl,
|
|
118
|
+
// Forward the client identifier from the standard MCP Mcp-Session-Id
|
|
119
|
+
// header if present, otherwise mark as 'hosted-http' so analytics
|
|
120
|
+
// can distinguish hosted-OAuth traffic from stdio traffic.
|
|
121
|
+
clientName: typeof req.headers['mcp-session-id'] === 'string'
|
|
122
|
+
? `hosted-${req.headers['mcp-session-id']}`
|
|
123
|
+
: 'hosted-http',
|
|
124
|
+
debug: baseEnv.debug,
|
|
125
|
+
});
|
|
126
|
+
const server = createMcpServer({ client, clientName: 'hosted-http' });
|
|
127
|
+
const transport = new StreamableHTTPServerTransport({
|
|
128
|
+
// Stateless mode: undefined sessionIdGenerator means no session
|
|
129
|
+
// tracking — each request is independent.
|
|
130
|
+
sessionIdGenerator: undefined,
|
|
131
|
+
});
|
|
132
|
+
// Clean up the per-request transport when the response closes so we
|
|
133
|
+
// don't leak connections under load.
|
|
134
|
+
res.on('close', () => {
|
|
135
|
+
void transport.close();
|
|
136
|
+
void server.close();
|
|
137
|
+
});
|
|
138
|
+
try {
|
|
139
|
+
await server.connect(transport);
|
|
140
|
+
await transport.handleRequest(req, res, req.body);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
if (baseEnv.debug) {
|
|
144
|
+
process.stderr.write(`[promptot-mcp-http] handler error: ${err instanceof Error ? err.stack : String(err)}\n`);
|
|
145
|
+
}
|
|
146
|
+
if (!res.headersSent) {
|
|
147
|
+
res.status(500).json({
|
|
148
|
+
error: 'server_error',
|
|
149
|
+
error_description: err instanceof Error ? err.message : 'Unknown error',
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
app.post('/mcp', (req, res) => {
|
|
155
|
+
void handleMcpRequest(req, res);
|
|
156
|
+
});
|
|
157
|
+
// GET /mcp is used by clients to open a long-lived SSE channel for
|
|
158
|
+
// server-initiated messages. We delegate to the same handler — the
|
|
159
|
+
// transport handles the GET vs POST distinction internally.
|
|
160
|
+
app.get('/mcp', (req, res) => {
|
|
161
|
+
void handleMcpRequest(req, res);
|
|
162
|
+
});
|
|
163
|
+
// DELETE /mcp tears down a session in stateful mode. We're stateless so
|
|
164
|
+
// this is a no-op, but we still respond 204 to be a well-behaved server.
|
|
165
|
+
app.delete('/mcp', (_req, res) => {
|
|
166
|
+
res.status(204).end();
|
|
167
|
+
});
|
|
168
|
+
// Generate a request ID header for the access logs (Railway uses this
|
|
169
|
+
// for tracing).
|
|
170
|
+
app.use((req, _res, next) => {
|
|
171
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
172
|
+
req.id = randomUUID();
|
|
173
|
+
next();
|
|
174
|
+
});
|
|
175
|
+
const server = app.listen(PORT, () => {
|
|
176
|
+
process.stderr.write(`[promptot-mcp-http] Listening on :${PORT} (api=${PUBLIC_API_URL})\n`);
|
|
177
|
+
});
|
|
178
|
+
// Graceful shutdown so Railway's deploy hand-off doesn't drop in-flight requests
|
|
179
|
+
function shutdown(signal) {
|
|
180
|
+
process.stderr.write(`[promptot-mcp-http] ${signal} received, shutting down\n`);
|
|
181
|
+
server.close(() => {
|
|
182
|
+
process.exit(0);
|
|
183
|
+
});
|
|
184
|
+
// Hard exit after 10s if connections won't close
|
|
185
|
+
setTimeout(() => process.exit(1), 10000).unref();
|
|
186
|
+
}
|
|
187
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
188
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
189
|
+
//# sourceMappingURL=http.js.map
|
package/dist/http.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/env.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment configuration for the PromptOT MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Read once at startup; fail fast if PROMPTOT_API_KEY is missing for the
|
|
5
|
+
* stdio entry point. The hosted HTTP entry point reads the per-session
|
|
6
|
+
* OAuth token instead and doesn't need a global API key.
|
|
7
|
+
*/
|
|
8
|
+
const DEFAULT_API_URL = 'https://api.promptot.com';
|
|
9
|
+
const DEFAULT_CLIENT_NAME = 'unknown';
|
|
10
|
+
function normalizeClientName(raw) {
|
|
11
|
+
if (!raw)
|
|
12
|
+
return DEFAULT_CLIENT_NAME;
|
|
13
|
+
const trimmed = raw.trim().toLowerCase();
|
|
14
|
+
if (!trimmed || trimmed.length > 50)
|
|
15
|
+
return DEFAULT_CLIENT_NAME;
|
|
16
|
+
return /^[a-z0-9_-]+$/.test(trimmed) ? trimmed : DEFAULT_CLIENT_NAME;
|
|
17
|
+
}
|
|
18
|
+
export function readEnv() {
|
|
19
|
+
return {
|
|
20
|
+
apiKey: process.env.PROMPTOT_API_KEY?.trim() || null,
|
|
21
|
+
apiUrl: (process.env.PROMPTOT_API_URL?.trim() || DEFAULT_API_URL).replace(/\/$/, ''),
|
|
22
|
+
clientName: normalizeClientName(process.env.PROMPTOT_MCP_CLIENT),
|
|
23
|
+
debug: process.env.PROMPTOT_DEBUG === 'true' || process.env.PROMPTOT_DEBUG === '1',
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function requireApiKey(env) {
|
|
27
|
+
if (!env.apiKey) {
|
|
28
|
+
// stderr because stdout is reserved for JSON-RPC over stdio
|
|
29
|
+
process.stderr.write('[promptot-mcp] ERROR: PROMPTOT_API_KEY is not set.\n' +
|
|
30
|
+
'Get a key from https://www.promptot.com/projects/<id>/api-keys and add it to your MCP config.\n');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=env.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.js","sourceRoot":"","sources":["../../src/lib/env.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,MAAM,eAAe,GAAG,0BAA0B,CAAC;AACnD,MAAM,mBAAmB,GAAG,SAAS,CAAC;AAEtC,SAAS,mBAAmB,CAAC,GAAuB;IAClD,IAAI,CAAC,GAAG;QAAE,OAAO,mBAAmB,CAAC;IACrC,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACzC,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,EAAE;QAAE,OAAO,mBAAmB,CAAC;IAChE,OAAO,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,mBAAmB,CAAC;AACvE,CAAC;AAED,MAAM,UAAU,OAAO;IACrB,OAAO;QACL,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,gBAAgB,EAAE,IAAI,EAAE,IAAI,IAAI;QACpD,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,EAAE,IAAI,EAAE,IAAI,eAAe,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;QACpF,UAAU,EAAE,mBAAmB,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC;QAChE,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc,KAAK,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,KAAK,GAAG;KACnF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;QAChB,4DAA4D;QAC5D,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,sDAAsD;YACpD,iGAAiG,CACpG,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PromptOT API error envelope shape and helpers for turning API errors into
|
|
3
|
+
* MCP-compatible error responses.
|
|
4
|
+
*/
|
|
5
|
+
export class PromptOTError extends Error {
|
|
6
|
+
status;
|
|
7
|
+
code;
|
|
8
|
+
details;
|
|
9
|
+
constructor(status, body) {
|
|
10
|
+
super(body.message);
|
|
11
|
+
this.name = 'PromptOTError';
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.code = body.code;
|
|
14
|
+
this.details = body.details;
|
|
15
|
+
}
|
|
16
|
+
toMcpContent() {
|
|
17
|
+
const lines = [`Error ${this.status} (${this.code}): ${this.message}`];
|
|
18
|
+
if (this.details) {
|
|
19
|
+
lines.push(`Details: ${JSON.stringify(this.details, null, 2)}`);
|
|
20
|
+
}
|
|
21
|
+
if (this.code === 'INSUFFICIENT_SCOPE') {
|
|
22
|
+
lines.push('This API key does not have the required scope for this tool. Regenerate the MCP key from the PromptOT dashboard with the appropriate scopes enabled.');
|
|
23
|
+
}
|
|
24
|
+
else if (this.code === 'UNAUTHORIZED') {
|
|
25
|
+
lines.push('Check that PROMPTOT_API_KEY is set correctly in your MCP client config.');
|
|
26
|
+
}
|
|
27
|
+
else if (this.code === 'API_QUOTA_EXCEEDED' || this.code === 'AI_CREDITS_EXHAUSTED') {
|
|
28
|
+
lines.push('Plan limit reached. Upgrade at https://www.promptot.com/billing.');
|
|
29
|
+
}
|
|
30
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function isPromptOTError(err) {
|
|
34
|
+
return err instanceof PromptOTError;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/lib/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH,MAAM,OAAO,aAAc,SAAQ,KAAK;IAC7B,MAAM,CAAS;IACf,IAAI,CAAS;IACb,OAAO,CAAU;IAE1B,YAAY,MAAc,EAAE,IAAuB;QACjD,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACpB,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;QAC5B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;IAC9B,CAAC;IAED,YAAY;QACV,MAAM,KAAK,GAAG,CAAC,SAAS,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QACvE,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,KAAK,CAAC,IAAI,CAAC,YAAY,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;QAClE,CAAC;QACD,IAAI,IAAI,CAAC,IAAI,KAAK,oBAAoB,EAAE,CAAC;YACvC,KAAK,CAAC,IAAI,CACR,sJAAsJ,CACvJ,CAAC;QACJ,CAAC;aAAM,IAAI,IAAI,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;YACxC,KAAK,CAAC,IAAI,CAAC,yEAAyE,CAAC,CAAC;QACxF,CAAC;aAAM,IAAI,IAAI,CAAC,IAAI,KAAK,oBAAoB,IAAI,IAAI,CAAC,IAAI,KAAK,sBAAsB,EAAE,CAAC;YACtF,KAAK,CAAC,IAAI,CAAC,kEAAkE,CAAC,CAAC;QACjF,CAAC;QACD,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACpD,CAAC;CACF;AAED,MAAM,UAAU,eAAe,CAAC,GAAY;IAC1C,OAAO,GAAG,YAAY,aAAa,CAAC;AACtC,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for formatting PromptOT API responses into MCP tool content blocks.
|
|
3
|
+
*
|
|
4
|
+
* Two principles drive this file:
|
|
5
|
+
* 1. Keep tool responses LLM-friendly — markdown headers and short JSON
|
|
6
|
+
* blocks rather than raw object dumps.
|
|
7
|
+
* 2. Truncate aggressive payloads (compiled prompts can hit 10k+ tokens)
|
|
8
|
+
* so a single tool call doesn't blow out the LLM's context window.
|
|
9
|
+
* Full text is still available via the promptot:// resource URIs.
|
|
10
|
+
*/
|
|
11
|
+
const CHARS_PER_TOKEN = 4; // matches the API's compilePrompt token estimate
|
|
12
|
+
export function truncateText(text, opts = {}) {
|
|
13
|
+
if (opts.truncate === false)
|
|
14
|
+
return text;
|
|
15
|
+
const maxTokens = opts.maxTokens ?? 2000;
|
|
16
|
+
const maxChars = maxTokens * CHARS_PER_TOKEN;
|
|
17
|
+
if (text.length <= maxChars)
|
|
18
|
+
return text;
|
|
19
|
+
const truncated = text.slice(0, maxChars);
|
|
20
|
+
const remainingTokens = Math.ceil((text.length - maxChars) / CHARS_PER_TOKEN);
|
|
21
|
+
const hint = opts.fullTextHint
|
|
22
|
+
? `\n\n... [truncated, ${remainingTokens} more tokens. ${opts.fullTextHint}]`
|
|
23
|
+
: `\n\n... [truncated, ${remainingTokens} more tokens]`;
|
|
24
|
+
return truncated + hint;
|
|
25
|
+
}
|
|
26
|
+
/** Wrap a JSON-serializable value into a single MCP text content block. */
|
|
27
|
+
export function jsonContent(value) {
|
|
28
|
+
return {
|
|
29
|
+
content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Wrap a plain string into a single MCP text content block. */
|
|
33
|
+
export function textContent(text) {
|
|
34
|
+
return {
|
|
35
|
+
content: [{ type: 'text', text }],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Wrap multiple lines into a single text content block. */
|
|
39
|
+
export function linesContent(...lines) {
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: 'text', text: lines.join('\n') }],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=format.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/lib/format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,eAAe,GAAG,CAAC,CAAC,CAAC,iDAAiD;AAW5E,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,OAAwB,EAAE;IACnE,IAAI,IAAI,CAAC,QAAQ,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IACzC,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC;IACzC,MAAM,QAAQ,GAAG,SAAS,GAAG,eAAe,CAAC;IAC7C,IAAI,IAAI,CAAC,MAAM,IAAI,QAAQ;QAAE,OAAO,IAAI,CAAC;IAEzC,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IAC1C,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC,GAAG,eAAe,CAAC,CAAC;IAC9E,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY;QAC5B,CAAC,CAAC,uBAAuB,eAAe,iBAAiB,IAAI,CAAC,YAAY,GAAG;QAC7E,CAAC,CAAC,uBAAuB,eAAe,eAAe,CAAC;IAC1D,OAAO,SAAS,GAAG,IAAI,CAAC;AAC1B,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,WAAW,CAAC,KAAc;IACxC,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;KAClE,CAAC;AACJ,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;KAClC,CAAC;AACJ,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,YAAY,CAAC,GAAG,KAAe;IAC7C,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;KACpD,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { isPromptOTError } from '../lib/errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* MCP resources for the PromptOT prompt catalog.
|
|
5
|
+
*
|
|
6
|
+
* Resources differ from tools: they're URI-addressable, read-only, and show up
|
|
7
|
+
* in the @ picker in clients like Claude Desktop. Users can drop them into a
|
|
8
|
+
* conversation as context without invoking a tool call.
|
|
9
|
+
*
|
|
10
|
+
* Three resources:
|
|
11
|
+
* - promptot://prompts — list of all prompts in the project
|
|
12
|
+
* - promptot://prompts/{prompt_id} — full prompt JSON with compiled output
|
|
13
|
+
* - promptot://prompts/{prompt_id}/markdown — compiled prompt as raw markdown
|
|
14
|
+
*
|
|
15
|
+
* The markdown form is the lazy escape hatch for tools that truncate compiled
|
|
16
|
+
* output to protect the LLM context window.
|
|
17
|
+
*/
|
|
18
|
+
export function registerPromptResources(server, client) {
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// promptot://prompts — list of all prompts
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
server.registerResource('prompts-list', 'promptot://prompts', {
|
|
23
|
+
title: 'PromptOT prompts',
|
|
24
|
+
description: 'List of all prompts in the project this MCP key is scoped to.',
|
|
25
|
+
mimeType: 'application/json',
|
|
26
|
+
}, async (uri) => {
|
|
27
|
+
try {
|
|
28
|
+
const data = await client.get('/api/v1/prompts', { tool: 'resource_list_prompts' });
|
|
29
|
+
return {
|
|
30
|
+
contents: [
|
|
31
|
+
{
|
|
32
|
+
uri: uri.href,
|
|
33
|
+
mimeType: 'application/json',
|
|
34
|
+
text: JSON.stringify(data, null, 2),
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
if (isPromptOTError(err)) {
|
|
41
|
+
return {
|
|
42
|
+
contents: [
|
|
43
|
+
{
|
|
44
|
+
uri: uri.href,
|
|
45
|
+
mimeType: 'text/plain',
|
|
46
|
+
text: `Error fetching prompts: ${err.message}`,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// promptot://prompts/{prompt_id} — full prompt as JSON
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
server.registerResource('prompt-detail', new ResourceTemplate('promptot://prompts/{prompt_id}', { list: undefined }), {
|
|
58
|
+
title: 'PromptOT prompt detail',
|
|
59
|
+
description: 'Full JSON of a single prompt including blocks, variables, and compiled output.',
|
|
60
|
+
mimeType: 'application/json',
|
|
61
|
+
}, async (uri, variables) => {
|
|
62
|
+
const promptId = String(variables.prompt_id);
|
|
63
|
+
try {
|
|
64
|
+
const data = await client.get(`/api/v1/prompts/${promptId}/full`, {
|
|
65
|
+
tool: 'resource_get_prompt',
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
contents: [
|
|
69
|
+
{
|
|
70
|
+
uri: uri.href,
|
|
71
|
+
mimeType: 'application/json',
|
|
72
|
+
text: JSON.stringify(data, null, 2),
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
if (isPromptOTError(err)) {
|
|
79
|
+
return {
|
|
80
|
+
contents: [
|
|
81
|
+
{
|
|
82
|
+
uri: uri.href,
|
|
83
|
+
mimeType: 'text/plain',
|
|
84
|
+
text: `Error fetching prompt ${promptId}: ${err.message}`,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// promptot://prompts/{prompt_id}/markdown — compiled prompt as markdown
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
server.registerResource('prompt-markdown', new ResourceTemplate('promptot://prompts/{prompt_id}/markdown', { list: undefined }), {
|
|
96
|
+
title: 'PromptOT compiled prompt markdown',
|
|
97
|
+
description: 'Compiled prompt of a PromptOT prompt rendered as raw markdown — useful for dropping the full text into a conversation as context.',
|
|
98
|
+
mimeType: 'text/markdown',
|
|
99
|
+
}, async (uri, variables) => {
|
|
100
|
+
const promptId = String(variables.prompt_id);
|
|
101
|
+
try {
|
|
102
|
+
const data = await client.get(`/api/v1/prompts/${promptId}/compile`, { tool: 'resource_get_prompt_markdown' });
|
|
103
|
+
return {
|
|
104
|
+
contents: [
|
|
105
|
+
{
|
|
106
|
+
uri: uri.href,
|
|
107
|
+
mimeType: 'text/markdown',
|
|
108
|
+
text: data.compiled_prompt,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
if (isPromptOTError(err)) {
|
|
115
|
+
return {
|
|
116
|
+
contents: [
|
|
117
|
+
{
|
|
118
|
+
uri: uri.href,
|
|
119
|
+
mimeType: 'text/plain',
|
|
120
|
+
text: `Error compiling prompt ${promptId}: ${err.message}`,
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=prompts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompts.js","sourceRoot":"","sources":["../../src/resources/prompts.ts"],"names":[],"mappings":"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"}
|