@getpultdev/mcp-server 0.1.0 → 0.2.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.
@@ -20,11 +20,14 @@ var PultOAuthProvider = class {
20
20
  clients = /* @__PURE__ */ new Map();
21
21
  pendingAuths = /* @__PURE__ */ new Map();
22
22
  authCodes = /* @__PURE__ */ new Map();
23
+ issuedTokens = /* @__PURE__ */ new Map();
23
24
  apiUrl;
24
- port;
25
- constructor(apiUrl, port) {
25
+ publicApiUrl;
26
+ baseUrl;
27
+ constructor(apiUrl, publicApiUrl, baseUrl) {
26
28
  this.apiUrl = apiUrl;
27
- this.port = port;
29
+ this.publicApiUrl = publicApiUrl.replace(/\/+$/, "");
30
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
28
31
  }
29
32
  get clientsStore() {
30
33
  return {
@@ -58,7 +61,7 @@ var PultOAuthProvider = class {
58
61
  if (!stored) throw new Error("Invalid authorization code");
59
62
  return stored.codeChallenge;
60
63
  }
61
- async exchangeAuthorizationCode(_client, authorizationCode) {
64
+ async exchangeAuthorizationCode(client, authorizationCode) {
62
65
  const stored = this.authCodes.get(authorizationCode);
63
66
  if (!stored) throw new Error("Invalid authorization code");
64
67
  if (Date.now() > stored.expiresAt) {
@@ -66,6 +69,7 @@ var PultOAuthProvider = class {
66
69
  throw new Error("Authorization code expired");
67
70
  }
68
71
  this.authCodes.delete(authorizationCode);
72
+ this.issuedTokens.set(client.client_id, stored.accessToken);
69
73
  return {
70
74
  access_token: stored.accessToken,
71
75
  token_type: "Bearer",
@@ -73,7 +77,7 @@ var PultOAuthProvider = class {
73
77
  expires_in: 86400
74
78
  };
75
79
  }
76
- async exchangeRefreshToken(_client, refreshToken) {
80
+ async exchangeRefreshToken(client, refreshToken) {
77
81
  const response = await fetch(`${this.apiUrl}/auth/refresh`, {
78
82
  method: "POST",
79
83
  headers: { "Content-Type": "application/json" },
@@ -81,26 +85,39 @@ var PultOAuthProvider = class {
81
85
  });
82
86
  if (!response.ok) throw new Error("Token refresh failed");
83
87
  const data = await response.json();
88
+ const accessToken = data["access_token"];
89
+ this.issuedTokens.set(client.client_id, accessToken);
84
90
  return {
85
- access_token: data["access_token"],
91
+ access_token: accessToken,
86
92
  token_type: "Bearer",
87
93
  refresh_token: data["refresh_token"],
88
94
  expires_in: data["expires_in"]
89
95
  };
90
96
  }
91
97
  async verifyAccessToken(token) {
98
+ const pultToken = this.resolveToken(token);
92
99
  const response = await fetch(`${this.apiUrl}/auth/me`, {
93
- headers: { Authorization: `Bearer ${token}` }
100
+ headers: { Authorization: `Bearer ${pultToken}` }
94
101
  });
95
102
  if (!response.ok) throw new Error("Invalid access token");
96
103
  const user = await response.json();
104
+ const parts = pultToken.split(".");
105
+ const expiresAt = parts.length === 3 ? JSON.parse(Buffer.from(parts[1], "base64url").toString()).exp : Math.floor(Date.now() / 1e3) + 86400;
97
106
  return {
98
- token,
107
+ token: pultToken,
99
108
  clientId: "pult",
100
109
  scopes: ["apps", "deploy", "db", "storage", "auth"],
110
+ expiresAt,
101
111
  extra: { userId: user["id"], email: user["email"] }
102
112
  };
103
113
  }
114
+ resolveToken(token) {
115
+ if (token.startsWith("eyJ")) return token;
116
+ for (const stored of this.issuedTokens.values()) {
117
+ return stored;
118
+ }
119
+ return token;
120
+ }
104
121
  handleCallback(sessionId, accessToken, refreshToken) {
105
122
  const pending = this.pendingAuths.get(sessionId);
106
123
  if (!pending) return null;
@@ -120,9 +137,9 @@ var PultOAuthProvider = class {
120
137
  return url.toString();
121
138
  }
122
139
  renderLoginPage(sessionId) {
123
- const callbackUrl = `http://localhost:${this.port}/oauth/callback/${sessionId}`;
124
- const githubUrl = `${this.apiUrl}/auth/github?redirect_uri=${encodeURIComponent(callbackUrl)}`;
125
- const googleUrl = `${this.apiUrl}/auth/google?redirect_uri=${encodeURIComponent(callbackUrl)}`;
140
+ const callbackUrl = `${this.baseUrl}/oauth/callback/${sessionId}`;
141
+ const githubUrl = `${this.publicApiUrl}/auth/github?redirect_uri=${encodeURIComponent(callbackUrl)}`;
142
+ const googleUrl = `${this.publicApiUrl}/auth/google?redirect_uri=${encodeURIComponent(callbackUrl)}`;
126
143
  return `<!DOCTYPE html>
127
144
  <html lang="en"><head>
128
145
  <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
@@ -164,7 +181,7 @@ e.preventDefault();
164
181
  const el=document.getElementById('e'),d=new FormData(e.target);
165
182
  el.style.display='none';
166
183
  try{
167
- const r=await fetch('${this.apiUrl}/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email:d.get('email'),password:d.get('password')})});
184
+ const r=await fetch('${this.publicApiUrl}/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email:d.get('email'),password:d.get('password')})});
168
185
  const j=await r.json();
169
186
  if(!r.ok){el.textContent=j.error||'Login failed';el.style.display='block';return}
170
187
  window.location.href='${callbackUrl}?access_token='+encodeURIComponent(j.access_token)+'&refresh_token='+encodeURIComponent(j.refresh_token);
@@ -177,15 +194,18 @@ window.location.href='${callbackUrl}?access_token='+encodeURIComponent(j.access_
177
194
 
178
195
  // src/http-server.ts
179
196
  var API_URL = process.env["PULT_API_URL"] ?? "https://api.pult.rest";
197
+ var PUBLIC_API_URL = process.env["PUBLIC_API_URL"] ?? "https://api.pult.rest";
180
198
  var PORT = parseInt(process.env["PORT"] ?? "3456", 10);
199
+ var PUBLIC_URL = process.env["PUBLIC_URL"] ?? `http://localhost:${PORT}`;
181
200
  function startHttpServer() {
182
201
  configure(API_URL, "");
183
202
  const app = express();
184
- const provider = new PultOAuthProvider(API_URL, PORT);
203
+ app.set("trust proxy", 1);
204
+ const provider = new PultOAuthProvider(API_URL, PUBLIC_API_URL, PUBLIC_URL);
185
205
  const sessions = /* @__PURE__ */ new Map();
186
206
  app.use(mcpAuthRouter({
187
207
  provider,
188
- issuerUrl: new URL(`http://localhost:${PORT}`),
208
+ issuerUrl: new URL(PUBLIC_URL),
189
209
  scopesSupported: ["apps", "deploy", "db", "storage", "auth"],
190
210
  resourceName: "Pult MCP Server",
191
211
  serviceDocumentationUrl: new URL("https://docs.pult.rest")
@@ -207,44 +227,48 @@ function startHttpServer() {
207
227
  });
208
228
  const bearerAuth = requireBearerAuth({ verifier: provider });
209
229
  app.all("/mcp", bearerAuth, async (req, res) => {
210
- const authInfo = req.auth;
211
- const sessionId = req.headers["mcp-session-id"];
212
- if (sessionId && sessions.has(sessionId)) {
213
- const session = sessions.get(sessionId);
214
- await withToken(session.token, () => session.transport.handleRequest(req, res));
215
- return;
216
- }
217
- if (req.method === "POST") {
218
- const transport = new StreamableHTTPServerTransport({
219
- sessionIdGenerator: () => randomUUID2(),
220
- onsessioninitialized: (id) => {
221
- sessions.set(id, { transport, token: authInfo.token });
222
- }
223
- });
224
- const mcpServer = createServer();
225
- await mcpServer.connect(transport);
226
- transport.onclose = () => {
227
- const id = transport.sessionId;
228
- if (id) sessions.delete(id);
229
- };
230
- await withToken(authInfo.token, () => transport.handleRequest(req, res));
231
- return;
230
+ try {
231
+ const authInfo = req.auth;
232
+ const sessionId = req.headers["mcp-session-id"];
233
+ if (sessionId && sessions.has(sessionId)) {
234
+ const session = sessions.get(sessionId);
235
+ await withToken(session.token, () => session.transport.handleRequest(req, res));
236
+ return;
237
+ }
238
+ if (req.method === "POST") {
239
+ const transport = new StreamableHTTPServerTransport({
240
+ sessionIdGenerator: () => randomUUID2(),
241
+ onsessioninitialized: (id) => {
242
+ sessions.set(id, { transport, token: authInfo.token });
243
+ }
244
+ });
245
+ const mcpServer = createServer();
246
+ await mcpServer.connect(transport);
247
+ transport.onclose = () => {
248
+ const id = transport.sessionId;
249
+ if (id) sessions.delete(id);
250
+ };
251
+ await withToken(authInfo.token, () => transport.handleRequest(req, res));
252
+ return;
253
+ }
254
+ res.status(400).json({ error: "No active session. Send an initialize request first." });
255
+ } catch (err) {
256
+ process.stderr.write(`MCP error: ${err}
257
+ `);
258
+ if (!res.headersSent) res.status(500).json({ error: "Internal server error" });
232
259
  }
233
- res.status(400).json({ error: "No active session. Send an initialize request first." });
234
260
  });
235
261
  app.get("/health", (_req, res) => {
236
- res.json({ status: "ok", transport: "http", tools: 186 });
262
+ res.json({ status: "ok", transport: "http" });
237
263
  });
238
264
  app.listen(PORT, () => {
239
- process.stderr.write(`Pult MCP server listening on http://localhost:${PORT}
240
- `);
241
- process.stderr.write(`MCP endpoint: http://localhost:${PORT}/mcp
265
+ process.stderr.write(`Pult MCP server listening on ${PUBLIC_URL}
242
266
  `);
243
- process.stderr.write(`OAuth metadata: http://localhost:${PORT}/.well-known/oauth-protected-resource
267
+ process.stderr.write(`MCP endpoint: ${PUBLIC_URL}/mcp
244
268
  `);
245
269
  });
246
270
  }
247
271
  export {
248
272
  startHttpServer
249
273
  };
250
- //# sourceMappingURL=http-server-FZEWVMOZ.js.map
274
+ //# sourceMappingURL=http-server-LQRNF2V4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/http-server.ts","../src/oauth-provider.ts"],"sourcesContent":["import express from \"express\"\nimport { randomUUID } from \"node:crypto\"\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\"\nimport { mcpAuthRouter } from \"@modelcontextprotocol/sdk/server/auth/router.js\"\nimport { requireBearerAuth } from \"@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js\"\nimport { createServer } from \"./server.js\"\nimport { configure, withToken } from \"./http.js\"\nimport { PultOAuthProvider } from \"./oauth-provider.js\"\n\nconst API_URL = process.env[\"PULT_API_URL\"] ?? \"https://api.pult.rest\"\nconst PUBLIC_API_URL = process.env[\"PUBLIC_API_URL\"] ?? \"https://api.pult.rest\"\nconst PORT = parseInt(process.env[\"PORT\"] ?? \"3456\", 10)\nconst PUBLIC_URL = process.env[\"PUBLIC_URL\"] ?? `http://localhost:${PORT}`\n\ninterface Session {\n transport: StreamableHTTPServerTransport\n token: string\n}\n\nexport function startHttpServer(): void {\n configure(API_URL, \"\")\n const app = express()\n app.set(\"trust proxy\", 1)\n const provider = new PultOAuthProvider(API_URL, PUBLIC_API_URL, PUBLIC_URL)\n const sessions = new Map<string, Session>()\n\n app.use(mcpAuthRouter({\n provider,\n issuerUrl: new URL(PUBLIC_URL),\n scopesSupported: [\"apps\", \"deploy\", \"db\", \"storage\", \"auth\"],\n resourceName: \"Pult MCP Server\",\n serviceDocumentationUrl: new URL(\"https://docs.pult.rest\"),\n }))\n\n app.get(\"/oauth/callback/:sessionId\", (req, res) => {\n const { sessionId } = req.params\n const accessToken = req.query[\"access_token\"] as string | undefined\n const refreshToken = (req.query[\"refresh_token\"] as string | undefined) ?? \"\"\n\n if (!sessionId || !accessToken) {\n res.status(400).send(\"Missing parameters\")\n return\n }\n\n const redirectUrl = provider.handleCallback(sessionId, accessToken, refreshToken)\n if (!redirectUrl) {\n res.status(400).send(\"Invalid or expired session\")\n return\n }\n\n res.redirect(redirectUrl)\n })\n\n const bearerAuth = requireBearerAuth({ verifier: provider })\n\n app.all(\"/mcp\", bearerAuth, async (req, res) => {\n try {\n const authInfo = req.auth!\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined\n\n if (sessionId && sessions.has(sessionId)) {\n const session = sessions.get(sessionId)!\n await withToken(session.token, () => session.transport.handleRequest(req, res))\n return\n }\n\n if (req.method === \"POST\") {\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n onsessioninitialized: (id) => {\n sessions.set(id, { transport, token: authInfo.token })\n },\n })\n\n const mcpServer = createServer()\n await mcpServer.connect(transport)\n\n transport.onclose = () => {\n const id = transport.sessionId\n if (id) sessions.delete(id)\n }\n\n await withToken(authInfo.token, () => transport.handleRequest(req, res))\n return\n }\n\n res.status(400).json({ error: \"No active session. Send an initialize request first.\" })\n } catch (err) {\n process.stderr.write(`MCP error: ${err}\\n`)\n if (!res.headersSent) res.status(500).json({ error: \"Internal server error\" })\n }\n })\n\n app.get(\"/health\", (_req, res) => {\n res.json({ status: \"ok\", transport: \"http\" })\n })\n\n app.listen(PORT, () => {\n process.stderr.write(`Pult MCP server listening on ${PUBLIC_URL}\\n`)\n process.stderr.write(`MCP endpoint: ${PUBLIC_URL}/mcp\\n`)\n })\n}\n","import { randomUUID, randomBytes } from \"node:crypto\"\nimport type { Response } from \"express\"\nimport type {\n OAuthServerProvider,\n AuthorizationParams,\n} from \"@modelcontextprotocol/sdk/server/auth/provider.js\"\nimport type { OAuthRegisteredClientsStore } from \"@modelcontextprotocol/sdk/server/auth/clients.js\"\nimport type { OAuthClientInformationFull, OAuthTokens } from \"@modelcontextprotocol/sdk/shared/auth.js\"\nimport type { AuthInfo } from \"@modelcontextprotocol/sdk/server/auth/types.js\"\n\ninterface PendingAuth {\n codeChallenge: string\n redirectUri: string\n state?: string\n clientId: string\n}\n\ninterface StoredCode {\n clientId: string\n codeChallenge: string\n redirectUri: string\n accessToken: string\n refreshToken: string\n expiresAt: number\n}\n\nexport class PultOAuthProvider implements OAuthServerProvider {\n private clients = new Map<string, OAuthClientInformationFull>()\n private pendingAuths = new Map<string, PendingAuth>()\n private authCodes = new Map<string, StoredCode>()\n private issuedTokens = new Map<string, string>()\n private apiUrl: string\n private publicApiUrl: string\n private baseUrl: string\n\n constructor(apiUrl: string, publicApiUrl: string, baseUrl: string) {\n this.apiUrl = apiUrl\n this.publicApiUrl = publicApiUrl.replace(/\\/+$/, \"\")\n this.baseUrl = baseUrl.replace(/\\/+$/, \"\")\n }\n\n get clientsStore(): OAuthRegisteredClientsStore {\n return {\n getClient: (id: string) => this.clients.get(id),\n registerClient: (metadata: Omit<OAuthClientInformationFull, \"client_id\" | \"client_id_issued_at\">) => {\n const clientId = randomUUID()\n const client = {\n ...metadata,\n client_id: clientId,\n client_id_issued_at: Math.floor(Date.now() / 1000),\n } as OAuthClientInformationFull\n this.clients.set(clientId, client)\n return client\n },\n }\n }\n\n async authorize(\n client: OAuthClientInformationFull,\n params: AuthorizationParams,\n res: Response,\n ): Promise<void> {\n const sessionId = randomUUID()\n this.pendingAuths.set(sessionId, {\n codeChallenge: params.codeChallenge,\n redirectUri: params.redirectUri,\n state: params.state,\n clientId: client.client_id,\n })\n\n const html = this.renderLoginPage(sessionId)\n res.setHeader(\"Content-Type\", \"text/html\")\n res.end(html)\n }\n\n async challengeForAuthorizationCode(\n _client: OAuthClientInformationFull,\n authorizationCode: string,\n ): Promise<string> {\n const stored = this.authCodes.get(authorizationCode)\n if (!stored) throw new Error(\"Invalid authorization code\")\n return stored.codeChallenge\n }\n\n async exchangeAuthorizationCode(\n client: OAuthClientInformationFull,\n authorizationCode: string,\n ): Promise<OAuthTokens> {\n const stored = this.authCodes.get(authorizationCode)\n if (!stored) throw new Error(\"Invalid authorization code\")\n if (Date.now() > stored.expiresAt) {\n this.authCodes.delete(authorizationCode)\n throw new Error(\"Authorization code expired\")\n }\n this.authCodes.delete(authorizationCode)\n this.issuedTokens.set(client.client_id, stored.accessToken)\n return {\n access_token: stored.accessToken,\n token_type: \"Bearer\",\n refresh_token: stored.refreshToken,\n expires_in: 86400,\n }\n }\n\n async exchangeRefreshToken(\n client: OAuthClientInformationFull,\n refreshToken: string,\n ): Promise<OAuthTokens> {\n const response = await fetch(`${this.apiUrl}/auth/refresh`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ refresh_token: refreshToken }),\n })\n if (!response.ok) throw new Error(\"Token refresh failed\")\n const data = (await response.json()) as Record<string, unknown>\n const accessToken = data[\"access_token\"] as string\n this.issuedTokens.set(client.client_id, accessToken)\n return {\n access_token: accessToken,\n token_type: \"Bearer\",\n refresh_token: data[\"refresh_token\"] as string,\n expires_in: data[\"expires_in\"] as number,\n }\n }\n\n async verifyAccessToken(token: string): Promise<AuthInfo> {\n const pultToken = this.resolveToken(token)\n const response = await fetch(`${this.apiUrl}/auth/me`, {\n headers: { Authorization: `Bearer ${pultToken}` },\n })\n if (!response.ok) throw new Error(\"Invalid access token\")\n const user = (await response.json()) as Record<string, unknown>\n const parts = pultToken.split(\".\")\n const expiresAt = parts.length === 3\n ? (JSON.parse(Buffer.from(parts[1]!, \"base64url\").toString()) as { exp?: number }).exp\n : Math.floor(Date.now() / 1000) + 86400\n return {\n token: pultToken,\n clientId: \"pult\",\n scopes: [\"apps\", \"deploy\", \"db\", \"storage\", \"auth\"],\n expiresAt,\n extra: { userId: user[\"id\"], email: user[\"email\"] },\n }\n }\n\n private resolveToken(token: string): string {\n if (token.startsWith(\"eyJ\")) return token\n for (const stored of this.issuedTokens.values()) {\n return stored\n }\n return token\n }\n\n handleCallback(sessionId: string, accessToken: string, refreshToken: string): string | null {\n const pending = this.pendingAuths.get(sessionId)\n if (!pending) return null\n this.pendingAuths.delete(sessionId)\n\n const code = randomBytes(32).toString(\"hex\")\n this.authCodes.set(code, {\n clientId: pending.clientId,\n codeChallenge: pending.codeChallenge,\n redirectUri: pending.redirectUri,\n accessToken,\n refreshToken,\n expiresAt: Date.now() + 5 * 60 * 1000,\n })\n\n const url = new URL(pending.redirectUri)\n url.searchParams.set(\"code\", code)\n if (pending.state) url.searchParams.set(\"state\", pending.state)\n return url.toString()\n }\n\n private renderLoginPage(sessionId: string): string {\n const callbackUrl = `${this.baseUrl}/oauth/callback/${sessionId}`\n const githubUrl = `${this.publicApiUrl}/auth/github?redirect_uri=${encodeURIComponent(callbackUrl)}`\n const googleUrl = `${this.publicApiUrl}/auth/google?redirect_uri=${encodeURIComponent(callbackUrl)}`\n\n return `<!DOCTYPE html>\n<html lang=\"en\"><head>\n<meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<title>Sign in to Pult</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\nbody{font-family:system-ui,-apple-system,sans-serif;display:flex;justify-content:center;align-items:center;min-height:100vh;background:#0a0a0a;color:#fff}\n.card{background:#141414;border:1px solid #262626;border-radius:16px;padding:40px;width:100%;max-width:380px}\nh1{font-size:22px;font-weight:600;margin-bottom:6px}\n.sub{color:#737373;font-size:14px;margin-bottom:32px}\n.btn{display:block;width:100%;padding:11px 16px;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;text-decoration:none;text-align:center;border:1px solid #262626;background:#1a1a1a;color:#e5e5e5;margin-bottom:10px;transition:background .15s}\n.btn:hover{background:#262626}\n.sep{display:flex;align-items:center;gap:12px;margin:24px 0;color:#525252;font-size:12px}\n.sep::before,.sep::after{content:'';flex:1;height:1px;background:#262626}\ninput{width:100%;padding:10px 12px;border-radius:8px;border:1px solid #262626;background:#0a0a0a;color:#fff;font-size:14px;margin-bottom:10px;outline:none;transition:border-color .15s}\ninput:focus{border-color:#525252}\n.btn-primary{background:#fff;color:#0a0a0a;border-color:#fff;font-weight:600}\n.btn-primary:hover{background:#d4d4d4}\n.err{color:#ef4444;font-size:13px;display:none;margin-bottom:10px}\n.logo{font-size:28px;font-weight:700;margin-bottom:24px;letter-spacing:-0.5px}\n</style></head>\n<body><div class=\"card\">\n<div class=\"logo\">Pult</div>\n<h1>Sign in</h1>\n<p class=\"sub\">Connect your account to continue</p>\n<a href=\"${githubUrl}\" class=\"btn\">Continue with GitHub</a>\n<a href=\"${googleUrl}\" class=\"btn\">Continue with Google</a>\n<div class=\"sep\">or</div>\n<form id=\"f\">\n<input type=\"email\" name=\"email\" placeholder=\"Email\" required autocomplete=\"email\">\n<input type=\"password\" name=\"password\" placeholder=\"Password\" required autocomplete=\"current-password\">\n<div class=\"err\" id=\"e\"></div>\n<button type=\"submit\" class=\"btn btn-primary\">Sign in with email</button>\n</form>\n</div>\n<script>\ndocument.getElementById('f').addEventListener('submit',async e=>{\ne.preventDefault();\nconst el=document.getElementById('e'),d=new FormData(e.target);\nel.style.display='none';\ntry{\nconst r=await fetch('${this.publicApiUrl}/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email:d.get('email'),password:d.get('password')})});\nconst j=await r.json();\nif(!r.ok){el.textContent=j.error||'Login failed';el.style.display='block';return}\nwindow.location.href='${callbackUrl}?access_token='+encodeURIComponent(j.access_token)+'&refresh_token='+encodeURIComponent(j.refresh_token);\n}catch(_){el.textContent='Network error';el.style.display='block'}\n});\n</script>\n</body></html>`\n }\n}\n"],"mappings":"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","names":["randomUUID","randomUUID"]}
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  // src/index.ts
7
7
  var transport = process.argv.includes("--http") || process.env["PULT_TRANSPORT"] === "http" ? "http" : "stdio";
8
8
  if (transport === "http") {
9
- const { startHttpServer } = await import("./http-server-FZEWVMOZ.js");
9
+ const { startHttpServer } = await import("./http-server-LQRNF2V4.js");
10
10
  startHttpServer();
11
11
  } else {
12
12
  const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getpultdev/mcp-server",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "MCP server for Pult platform — manage apps, deployments, databases, and more from AI tools",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/http-server.ts","../src/oauth-provider.ts"],"sourcesContent":["import express from \"express\"\nimport { randomUUID } from \"node:crypto\"\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\"\nimport { mcpAuthRouter } from \"@modelcontextprotocol/sdk/server/auth/router.js\"\nimport { requireBearerAuth } from \"@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js\"\nimport { createServer } from \"./server.js\"\nimport { configure, withToken } from \"./http.js\"\nimport { PultOAuthProvider } from \"./oauth-provider.js\"\n\nconst API_URL = process.env[\"PULT_API_URL\"] ?? \"https://api.pult.rest\"\nconst PORT = parseInt(process.env[\"PORT\"] ?? \"3456\", 10)\n\ninterface Session {\n transport: StreamableHTTPServerTransport\n token: string\n}\n\nexport function startHttpServer(): void {\n configure(API_URL, \"\")\n const app = express()\n const provider = new PultOAuthProvider(API_URL, PORT)\n const sessions = new Map<string, Session>()\n\n app.use(mcpAuthRouter({\n provider,\n issuerUrl: new URL(`http://localhost:${PORT}`),\n scopesSupported: [\"apps\", \"deploy\", \"db\", \"storage\", \"auth\"],\n resourceName: \"Pult MCP Server\",\n serviceDocumentationUrl: new URL(\"https://docs.pult.rest\"),\n }))\n\n app.get(\"/oauth/callback/:sessionId\", (req, res) => {\n const { sessionId } = req.params\n const accessToken = req.query[\"access_token\"] as string | undefined\n const refreshToken = (req.query[\"refresh_token\"] as string | undefined) ?? \"\"\n\n if (!sessionId || !accessToken) {\n res.status(400).send(\"Missing parameters\")\n return\n }\n\n const redirectUrl = provider.handleCallback(sessionId, accessToken, refreshToken)\n if (!redirectUrl) {\n res.status(400).send(\"Invalid or expired session\")\n return\n }\n\n res.redirect(redirectUrl)\n })\n\n const bearerAuth = requireBearerAuth({ verifier: provider })\n\n app.all(\"/mcp\", bearerAuth, async (req, res) => {\n const authInfo = req.auth!\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined\n\n if (sessionId && sessions.has(sessionId)) {\n const session = sessions.get(sessionId)!\n await withToken(session.token, () => session.transport.handleRequest(req, res))\n return\n }\n\n if (req.method === \"POST\") {\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n onsessioninitialized: (id) => {\n sessions.set(id, { transport, token: authInfo.token })\n },\n })\n\n const mcpServer = createServer()\n await mcpServer.connect(transport)\n\n transport.onclose = () => {\n const id = transport.sessionId\n if (id) sessions.delete(id)\n }\n\n await withToken(authInfo.token, () => transport.handleRequest(req, res))\n return\n }\n\n res.status(400).json({ error: \"No active session. Send an initialize request first.\" })\n })\n\n app.get(\"/health\", (_req, res) => {\n res.json({ status: \"ok\", transport: \"http\", tools: 186 })\n })\n\n app.listen(PORT, () => {\n process.stderr.write(`Pult MCP server listening on http://localhost:${PORT}\\n`)\n process.stderr.write(`MCP endpoint: http://localhost:${PORT}/mcp\\n`)\n process.stderr.write(`OAuth metadata: http://localhost:${PORT}/.well-known/oauth-protected-resource\\n`)\n })\n}\n","import { randomUUID, randomBytes } from \"node:crypto\"\nimport type { Response } from \"express\"\nimport type {\n OAuthServerProvider,\n AuthorizationParams,\n} from \"@modelcontextprotocol/sdk/server/auth/provider.js\"\nimport type { OAuthRegisteredClientsStore } from \"@modelcontextprotocol/sdk/server/auth/clients.js\"\nimport type { OAuthClientInformationFull, OAuthTokens } from \"@modelcontextprotocol/sdk/shared/auth.js\"\nimport type { AuthInfo } from \"@modelcontextprotocol/sdk/server/auth/types.js\"\n\ninterface PendingAuth {\n codeChallenge: string\n redirectUri: string\n state?: string\n clientId: string\n}\n\ninterface StoredCode {\n clientId: string\n codeChallenge: string\n redirectUri: string\n accessToken: string\n refreshToken: string\n expiresAt: number\n}\n\nexport class PultOAuthProvider implements OAuthServerProvider {\n private clients = new Map<string, OAuthClientInformationFull>()\n private pendingAuths = new Map<string, PendingAuth>()\n private authCodes = new Map<string, StoredCode>()\n private apiUrl: string\n private port: number\n\n constructor(apiUrl: string, port: number) {\n this.apiUrl = apiUrl\n this.port = port\n }\n\n get clientsStore(): OAuthRegisteredClientsStore {\n return {\n getClient: (id: string) => this.clients.get(id),\n registerClient: (metadata: Omit<OAuthClientInformationFull, \"client_id\" | \"client_id_issued_at\">) => {\n const clientId = randomUUID()\n const client = {\n ...metadata,\n client_id: clientId,\n client_id_issued_at: Math.floor(Date.now() / 1000),\n } as OAuthClientInformationFull\n this.clients.set(clientId, client)\n return client\n },\n }\n }\n\n async authorize(\n client: OAuthClientInformationFull,\n params: AuthorizationParams,\n res: Response,\n ): Promise<void> {\n const sessionId = randomUUID()\n this.pendingAuths.set(sessionId, {\n codeChallenge: params.codeChallenge,\n redirectUri: params.redirectUri,\n state: params.state,\n clientId: client.client_id,\n })\n\n const html = this.renderLoginPage(sessionId)\n res.setHeader(\"Content-Type\", \"text/html\")\n res.end(html)\n }\n\n async challengeForAuthorizationCode(\n _client: OAuthClientInformationFull,\n authorizationCode: string,\n ): Promise<string> {\n const stored = this.authCodes.get(authorizationCode)\n if (!stored) throw new Error(\"Invalid authorization code\")\n return stored.codeChallenge\n }\n\n async exchangeAuthorizationCode(\n _client: OAuthClientInformationFull,\n authorizationCode: string,\n ): Promise<OAuthTokens> {\n const stored = this.authCodes.get(authorizationCode)\n if (!stored) throw new Error(\"Invalid authorization code\")\n if (Date.now() > stored.expiresAt) {\n this.authCodes.delete(authorizationCode)\n throw new Error(\"Authorization code expired\")\n }\n this.authCodes.delete(authorizationCode)\n return {\n access_token: stored.accessToken,\n token_type: \"Bearer\",\n refresh_token: stored.refreshToken,\n expires_in: 86400,\n }\n }\n\n async exchangeRefreshToken(\n _client: OAuthClientInformationFull,\n refreshToken: string,\n ): Promise<OAuthTokens> {\n const response = await fetch(`${this.apiUrl}/auth/refresh`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ refresh_token: refreshToken }),\n })\n if (!response.ok) throw new Error(\"Token refresh failed\")\n const data = (await response.json()) as Record<string, unknown>\n return {\n access_token: data[\"access_token\"] as string,\n token_type: \"Bearer\",\n refresh_token: data[\"refresh_token\"] as string,\n expires_in: data[\"expires_in\"] as number,\n }\n }\n\n async verifyAccessToken(token: string): Promise<AuthInfo> {\n const response = await fetch(`${this.apiUrl}/auth/me`, {\n headers: { Authorization: `Bearer ${token}` },\n })\n if (!response.ok) throw new Error(\"Invalid access token\")\n const user = (await response.json()) as Record<string, unknown>\n return {\n token,\n clientId: \"pult\",\n scopes: [\"apps\", \"deploy\", \"db\", \"storage\", \"auth\"],\n extra: { userId: user[\"id\"], email: user[\"email\"] },\n }\n }\n\n handleCallback(sessionId: string, accessToken: string, refreshToken: string): string | null {\n const pending = this.pendingAuths.get(sessionId)\n if (!pending) return null\n this.pendingAuths.delete(sessionId)\n\n const code = randomBytes(32).toString(\"hex\")\n this.authCodes.set(code, {\n clientId: pending.clientId,\n codeChallenge: pending.codeChallenge,\n redirectUri: pending.redirectUri,\n accessToken,\n refreshToken,\n expiresAt: Date.now() + 5 * 60 * 1000,\n })\n\n const url = new URL(pending.redirectUri)\n url.searchParams.set(\"code\", code)\n if (pending.state) url.searchParams.set(\"state\", pending.state)\n return url.toString()\n }\n\n private renderLoginPage(sessionId: string): string {\n const callbackUrl = `http://localhost:${this.port}/oauth/callback/${sessionId}`\n const githubUrl = `${this.apiUrl}/auth/github?redirect_uri=${encodeURIComponent(callbackUrl)}`\n const googleUrl = `${this.apiUrl}/auth/google?redirect_uri=${encodeURIComponent(callbackUrl)}`\n\n return `<!DOCTYPE html>\n<html lang=\"en\"><head>\n<meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<title>Sign in to Pult</title>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\nbody{font-family:system-ui,-apple-system,sans-serif;display:flex;justify-content:center;align-items:center;min-height:100vh;background:#0a0a0a;color:#fff}\n.card{background:#141414;border:1px solid #262626;border-radius:16px;padding:40px;width:100%;max-width:380px}\nh1{font-size:22px;font-weight:600;margin-bottom:6px}\n.sub{color:#737373;font-size:14px;margin-bottom:32px}\n.btn{display:block;width:100%;padding:11px 16px;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;text-decoration:none;text-align:center;border:1px solid #262626;background:#1a1a1a;color:#e5e5e5;margin-bottom:10px;transition:background .15s}\n.btn:hover{background:#262626}\n.sep{display:flex;align-items:center;gap:12px;margin:24px 0;color:#525252;font-size:12px}\n.sep::before,.sep::after{content:'';flex:1;height:1px;background:#262626}\ninput{width:100%;padding:10px 12px;border-radius:8px;border:1px solid #262626;background:#0a0a0a;color:#fff;font-size:14px;margin-bottom:10px;outline:none;transition:border-color .15s}\ninput:focus{border-color:#525252}\n.btn-primary{background:#fff;color:#0a0a0a;border-color:#fff;font-weight:600}\n.btn-primary:hover{background:#d4d4d4}\n.err{color:#ef4444;font-size:13px;display:none;margin-bottom:10px}\n.logo{font-size:28px;font-weight:700;margin-bottom:24px;letter-spacing:-0.5px}\n</style></head>\n<body><div class=\"card\">\n<div class=\"logo\">Pult</div>\n<h1>Sign in</h1>\n<p class=\"sub\">Connect your account to continue</p>\n<a href=\"${githubUrl}\" class=\"btn\">Continue with GitHub</a>\n<a href=\"${googleUrl}\" class=\"btn\">Continue with Google</a>\n<div class=\"sep\">or</div>\n<form id=\"f\">\n<input type=\"email\" name=\"email\" placeholder=\"Email\" required autocomplete=\"email\">\n<input type=\"password\" name=\"password\" placeholder=\"Password\" required autocomplete=\"current-password\">\n<div class=\"err\" id=\"e\"></div>\n<button type=\"submit\" class=\"btn btn-primary\">Sign in with email</button>\n</form>\n</div>\n<script>\ndocument.getElementById('f').addEventListener('submit',async e=>{\ne.preventDefault();\nconst el=document.getElementById('e'),d=new FormData(e.target);\nel.style.display='none';\ntry{\nconst r=await fetch('${this.apiUrl}/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email:d.get('email'),password:d.get('password')})});\nconst j=await r.json();\nif(!r.ok){el.textContent=j.error||'Login failed';el.style.display='block';return}\nwindow.location.href='${callbackUrl}?access_token='+encodeURIComponent(j.access_token)+'&refresh_token='+encodeURIComponent(j.refresh_token);\n}catch(_){el.textContent='Network error';el.style.display='block'}\n});\n</script>\n</body></html>`\n }\n}\n"],"mappings":"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","names":["randomUUID","randomUUID"]}