@mongoosejs/studio 0.6.1 → 0.7.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/CHANGELOG.md CHANGED
@@ -1,5 +1,63 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### MCP support for Next.js and Vercel
6
+
7
+ Mongoose Studio's MCP endpoint now works on Next.js and standalone Vercel Functions, not only Express.
8
+ MCP uses the same defaults as Express: it is enabled by default without an `apiKey`, and with an `apiKey` it requires `mcp: true` and `publicUrl` and uses OAuth.
9
+
10
+ For Next.js, rename `pages/api/studio.js` to the optional catch-all route `pages/api/studio/[[...studioRoute]].js`.
11
+ The same `vercelAPIRouteHandler()` handles Studio actions at `/api/studio`, MCP at `/api/studio/mcp`, and OAuth metadata at `/api/studio/.well-known/oauth-protected-resource`.
12
+ `withMongooseStudio()` now accepts a second `options` argument with `mcp`, `apiKey`, and `publicUrl`, and adds rewrites so MCP clients connect to `/studio/mcp`:
13
+
14
+ ```javascript
15
+ // next.config.js
16
+ export default withMongooseStudio({ reactStrictMode: true }, {
17
+ mcp: true,
18
+ publicUrl: process.env.MONGOOSE_STUDIO_PUBLIC_URL
19
+ });
20
+
21
+ // pages/api/studio/[[...studioRoute]].js
22
+ export default vercelAPIRouteHandler(db, {
23
+ apiKey: process.env.MONGOOSE_STUDIO_API_KEY,
24
+ mcp: true,
25
+ publicUrl: process.env.MONGOOSE_STUDIO_PUBLIC_URL
26
+ });
27
+ ```
28
+
29
+ For standalone Vercel Functions, keep `api/studio.js` for Studio actions and add `api/studio-mcp.js` using the new `vercelMCPRouteHandler()`:
30
+
31
+ ```javascript
32
+ // api/studio-mcp.js
33
+ const { vercelMCPRouteHandler } = require('@mongoosejs/studio/vercel');
34
+
35
+ module.exports = vercelMCPRouteHandler(mongoose, {
36
+ apiKey: process.env.MONGOOSE_STUDIO_API_KEY,
37
+ publicUrl: process.env.MONGOOSE_STUDIO_PUBLIC_URL
38
+ });
39
+ ```
40
+
41
+ With an `apiKey` and `mcp: true`, building the frontend with `frontend()` now writes the OAuth protected resource metadata to `frontend/public/.well-known/oauth-protected-resource` and registers `/api/studio-mcp` with the authorization server.
42
+ Copy the frontend only after the `frontend()` promise resolves, and make sure the copy includes hidden directories such as `.well-known`.
43
+ See the README for the full Vercel setup.
44
+
45
+ ### Breaking: Vercel and Next.js handlers without an API key are limited to localhost
46
+
47
+ `vercelAPIRouteHandler()` now applies the same `bindIp` restriction as the Express integration.
48
+ Without an `apiKey`, Studio API actions and MCP requests are only accepted from localhost by default, and other clients get a 403.
49
+ On Vercel, the client IP comes from the `x-vercel-forwarded-for` header, so a deployed handler without an `apiKey` rejects every request unless you configure `bindIp`.
50
+
51
+ Set `bindIp` to an IP address or an array of IP addresses to allow specific clients, or set `bindIp: null` to restore the previous behavior of accepting requests from any IP:
52
+
53
+ ```javascript
54
+ const handler = vercelAPIRouteHandler(db, {
55
+ bindIp: null
56
+ });
57
+ ```
58
+
59
+ Handlers configured with an `apiKey` are unaffected.
60
+
3
61
  ## 0.6.1
4
62
 
5
63
  ### Safer MCP defaults with API keys
package/README.md CHANGED
@@ -91,28 +91,109 @@ import withMongooseStudio from '@mongoosejs/studio/next';
91
91
  export default withMongooseStudio({
92
92
  // Your Next.js config here
93
93
  reactStrictMode: true,
94
+ }, {
95
+ mcp: true,
96
+ publicUrl: process.env.MONGOOSE_STUDIO_PUBLIC_URL
94
97
  });
95
98
  ```
96
99
 
97
- Then, add `pages/api/studio.js` to your Next.js project to host the Mongoose Studio API:
100
+ Add `pages/api/studio/[[...studioRoute]].js` to your Next.js project to host
101
+ both the Mongoose Studio API and MCP. If you already have
102
+ `pages/api/studio.js`, move its handler into the new file and remove the old
103
+ file. The optional catch-all keeps `/api/studio` working for existing Studio
104
+ actions and also accepts `/api/studio/mcp` and the OAuth metadata path.
98
105
 
99
106
  ```javascript
107
+ // pages/api/studio/[[...studioRoute]].js
100
108
  // Make sure to import the database connection
101
- import db from '../../src/db';
102
- import studio from '@mongoosejs/studio/backend/next';
109
+ import db from '../../../src/db';
110
+ import { vercelAPIRouteHandler } from '@mongoosejs/studio/vercel';
103
111
 
104
- const handler = studio(
112
+ const handler = vercelAPIRouteHandler(
105
113
  db, // Mongoose connection or Mongoose global. Or null to use `import mongoose`.
106
114
  {
107
115
  apiKey: process.env.MONGOOSE_STUDIO_API_KEY, // optional
108
116
  connection: db, // Optional: Connection or Mongoose global. If omitted, will use `import mongoose`
109
117
  connectToDB: async () => { /* connection logic here */ }, // Optional: if you need to call a function to connect to the database put it here
118
+ mcp: true, // Also enabled by default without an API key
119
+ publicUrl: process.env.MONGOOSE_STUDIO_PUBLIC_URL // Required with an API key; e.g. https://app.example.com/studio
110
120
  }
111
121
  );
112
122
 
113
123
  export default handler;
114
124
  ```
115
125
 
126
+ With `mcp: true`, the same API function handles Streamable HTTP
127
+ at `/studio/mcp`. With an API key, it also serves OAuth discovery at
128
+ `/studio/.well-known/oauth-protected-resource`. `withMongooseStudio` installs
129
+ rewrites from those public URLs to the catch-all API route and shows
130
+ `/studio/mcp` in the frontend. No additional API function or `mcpUrl` setting
131
+ is needed. The rewrites are needed to keep the public URL at `/studio/mcp`;
132
+ the catch-all file itself lives under `/api/studio`. A Pro API key is required
133
+ for OAuth-based Vercel MCP. Without an API key, MCP is enabled by default and
134
+ is limited to localhost unless `bindIp` is configured. Set `bindIp` to an IP
135
+ address or an array of IP addresses to permit other clients; set it to `null`
136
+ to allow access from any IP. The same restriction applies to Studio API actions.
137
+ With an API key, set `publicUrl` to the externally reachable `/studio` URL. The OAuth resource
138
+ URL is derived from it, even if the workspace's `baseUrl` differs.
139
+
140
+ The handler is stateless and returns a JSON response for each POST. No Redis,
141
+ sticky sessions, or long-lived SSE connection is needed. Tool calls still count
142
+ against Vercel Function duration and payload limits, so set an appropriate
143
+ `maxDuration` for longer database operations and keep results reasonably sized.
144
+
145
+ ### Standalone Vercel Functions
146
+
147
+ If your project uses `api/studio.js` rather than Next.js, keep it for Studio
148
+ actions and add a separate `api/studio-mcp.js` function for MCP. Next.js
149
+ optional catch-all filenames such as `[[...studioRoute]].js` do not apply to
150
+ standalone Vercel Functions:
151
+
152
+ ```javascript
153
+ // api/studio-mcp.js
154
+ // Make sure to import the database connection
155
+ import db from '../../../src/db';
156
+ const { vercelMCPRouteHandler } = require('@mongoosejs/studio/vercel');
157
+
158
+ module.exports = vercelMCPRouteHandler(db, {
159
+ apiKey: process.env.MONGOOSE_STUDIO_API_KEY,
160
+ publicUrl: process.env.MONGOOSE_STUDIO_PUBLIC_URL
161
+ });
162
+ ```
163
+
164
+ Set `MONGOOSE_STUDIO_PUBLIC_URL` to the externally reachable Studio URL, for
165
+ example `https://app.example.com/studio`, when using an API key. Without an
166
+ API key, MCP is enabled by default and follows the same localhost and `bindIp`
167
+ restriction described above. Connect MCP clients to `/api/studio-mcp`; no
168
+ rewrite is required.
169
+
170
+ For OAuth, the protected resource metadata is generated in the Studio frontend
171
+ by the same `frontend()` call used to write `config.js`. It registers the MCP
172
+ resource before the returned promise resolves, so copy the frontend after
173
+ that promise resolves. Existing `publicUrl` fallbacks can stay as they are;
174
+ the copy source must include hidden files:
175
+
176
+ ```javascript
177
+ const { execSync } = require('child_process');
178
+ const opts = {
179
+ apiKey: process.env.MONGOOSE_STUDIO_API_KEY,
180
+ mcp: true,
181
+ publicUrl: process.env.MONGOOSE_STUDIO_PUBLIC_URL
182
+ };
183
+ require('@mongoosejs/studio/frontend')('/api/studio', true, opts).then(() => {
184
+ execSync('mkdir -p ./public/studio && cp -r ./node_modules/@mongoosejs/studio/frontend/public/. ./public/studio/');
185
+ });
186
+ ```
187
+
188
+ The `/.` source copies hidden directories such as `.well-known`; a `/*` source
189
+ does not. This places `public/studio/.well-known/oauth-protected-resource`
190
+ and registers `/api/studio-mcp` with the authorization server. The metadata is
191
+ needed only with an API key. Set `Content-Type: application/json` and
192
+ `Access-Control-Allow-Origin: *` for that static path, as in the
193
+ [standalone Vercel example](development/movies-vercel/vercel.json). Its
194
+ [build script](development/movies-vercel/build.js) shows the complete asset
195
+ copy and metadata generation sequence.
196
+
116
197
  ### Nest.js
117
198
 
118
199
  Add `MongooseStudioModule` to your app module as follows.
package/backend/next.js CHANGED
@@ -1,17 +1,41 @@
1
1
  'use strict';
2
2
 
3
3
  const Backend = require('./');
4
+ const vercelMCP = require('./vercel/vercelMCP');
5
+ const checkVercelBindIp = require('./vercel/checkBindIp');
4
6
  const { defaultMothershipURL } = require('../constants');
5
7
 
6
8
  module.exports = function next(conn, options) {
7
9
  options = { ...options };
10
+ options.mcp = options.mcp ?? !options.apiKey;
11
+ const checkRequest = checkVercelBindIp(options);
8
12
  const backend = Backend(conn, options.studioConnection, options);
13
+ const mcpHandlers = options.mcp ? vercelMCP(conn, options, backend) : null;
9
14
 
10
15
  const mothershipUrl = options._mothershipUrl || defaultMothershipURL;
11
16
  let workspace = null;
12
17
 
13
18
  return async function wrappedNextJSFunction(req, res) {
19
+ if (!checkRequest(req, res)) {
20
+ return;
21
+ }
22
+
23
+ const route = req.query?.studioRoute;
24
+ if (route != null && (!Array.isArray(route) || route.length > 0)) {
25
+ const routePath = Array.isArray(route) ? route.join('/') : null;
26
+ if (routePath !== 'mcp' && routePath !== '.well-known/oauth-protected-resource') {
27
+ return res.status(404).json({ message: 'Not found' });
28
+ }
29
+ if (!mcpHandlers) {
30
+ return res.status(404).json({ message: 'Not found' });
31
+ }
32
+ return routePath === 'mcp'
33
+ ? mcpHandlers.endpoint(req, res)
34
+ : mcpHandlers.metadata(req, res);
35
+ }
36
+
14
37
  const params = { ...req.query, ...req.body, ...req.params, authorization: req.headers.authorization };
38
+ delete params.studioRoute;
15
39
  const actionName = params?.action;
16
40
 
17
41
  const authorization = params?.authorization;
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ const isBindIPConnection = require('../helpers/isBindIPConnection');
4
+ const isLocalhostConnection = require('../helpers/isLocalhostConnection');
5
+ const normalizeBindIPOption = require('../helpers/normalizeBindIPOption');
6
+
7
+ module.exports = function checkVercelBindIp(options) {
8
+ const hasBindIpOption = options.bindIp != null;
9
+ const bindIp = normalizeBindIPOption(options.bindIp);
10
+
11
+ return function checkRequest(req, res) {
12
+ if (options.apiKey || bindIp === null) {
13
+ return true;
14
+ }
15
+ const ip = (process.env.VERCEL && req.headers['x-vercel-forwarded-for']) ||
16
+ req.ip || req.socket?.remoteAddress || '';
17
+ const allowed = hasBindIpOption
18
+ ? isBindIPConnection({ ip }, bindIp)
19
+ : isLocalhostConnection({ ip });
20
+ if (!allowed) {
21
+ res.status(403).json({ message: 'Mongoose Studio without an API key only accepts localhost or configured bindIp connections' });
22
+ }
23
+ return allowed;
24
+ };
25
+ };
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+
3
+ module.exports = require('../next');
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ const { defaultMothershipURL } = require('../../constants');
4
+
5
+ module.exports = function vercelMCPConfig(options) {
6
+ if (!options.publicUrl) {
7
+ throw new Error('Mongoose Studio requires publicUrl when Vercel MCP is enabled with an API key');
8
+ }
9
+ const baseUrl = new URL(options.publicUrl);
10
+ baseUrl.search = '';
11
+ baseUrl.hash = '';
12
+ baseUrl.pathname = `${baseUrl.pathname.replace(/\/+$/, '')}/`;
13
+ const resource = new URL(options._mcpPath || 'mcp', baseUrl).toString();
14
+ const metadataUrl = new URL('.well-known/oauth-protected-resource', baseUrl).toString();
15
+ const mothershipUrl = options._mothershipUrl || defaultMothershipURL;
16
+
17
+ return {
18
+ resource,
19
+ metadataUrl,
20
+ metadata: {
21
+ resource,
22
+ authorization_servers: [new URL(mothershipUrl).origin],
23
+ scopes_supported: ['mcp'],
24
+ bearer_methods_supported: ['header'],
25
+ resource_documentation: 'https://mongoosestudio.app/docs/'
26
+ }
27
+ };
28
+ };
@@ -0,0 +1,152 @@
1
+ 'use strict';
2
+
3
+ const Backend = require('../');
4
+ const mcp = require('../mcp');
5
+ const mothership = require('../integrations/mothership');
6
+ const vercelMCPConfig = require('./mcpConfig');
7
+ const { defaultMothershipURL } = require('../../constants');
8
+
9
+ module.exports = function createVercelMCPHandlers(conn, options, existingBackend) {
10
+ if (!options.mcp) {
11
+ throw new Error('Mongoose Studio requires mcp: true for the Vercel MCP handler');
12
+ }
13
+ const config = options.apiKey ? vercelMCPConfig(options) : null;
14
+ const { resource, metadataUrl } = config || {};
15
+ const mothershipUrl = options._mothershipUrl || defaultMothershipURL;
16
+ const backend = existingBackend || Backend(conn, options.studioConnection, options);
17
+ let workspacePromise;
18
+ let registered = false;
19
+
20
+ async function workspace() {
21
+ if (!workspacePromise) {
22
+ workspacePromise = fetch(`${mothershipUrl}/getWorkspace`, {
23
+ method: 'POST',
24
+ body: JSON.stringify({ apiKey: options.apiKey }),
25
+ headers: { Authorization: `Bearer ${options.apiKey}`, 'Content-Type': 'application/json' }
26
+ }).then(async response => {
27
+ const data = await response.json();
28
+ if (!response.ok) {
29
+ throw new Error(data.message || `Mongoose Studio API Key Error ${response.status}`);
30
+ }
31
+ options.workspace = data.workspace;
32
+ return data.workspace;
33
+ }).catch(err => {
34
+ workspacePromise = null;
35
+ throw err;
36
+ });
37
+ }
38
+ return workspacePromise;
39
+ }
40
+
41
+ async function registerResource() {
42
+ if (registered) {
43
+ return;
44
+ }
45
+ try {
46
+ await mothership.Workspace.registerMCPResource({ apiKey: options.apiKey, resource }, options);
47
+ registered = true;
48
+ } catch (err) {
49
+ console.warn(`[MONGOOSE STUDIO] Could not register MCP resource ${resource}: ${err.message}`);
50
+ }
51
+ }
52
+
53
+ function challenge(res, error) {
54
+ const detail = error ? `, error="invalid_token", error_description="${error.replace(/"/g, '')}"` : '';
55
+ res.setHeader('WWW-Authenticate', `Bearer resource_metadata="${metadataUrl}"${detail}`);
56
+ }
57
+
58
+ function cors(req, res) {
59
+ res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*');
60
+ res.setHeader('Vary', 'Origin');
61
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
62
+ res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type, Mcp-Protocol-Version, Mcp-Session-Id, Last-Event-ID');
63
+ res.setHeader('Access-Control-Expose-Headers', 'WWW-Authenticate, Mcp-Session-Id, Mcp-Protocol-Version');
64
+ res.setHeader('Access-Control-Max-Age', '86400');
65
+ if (req.method === 'OPTIONS') {
66
+ res.status(204).end();
67
+ return true;
68
+ }
69
+ return false;
70
+ }
71
+
72
+ async function metadata(req, res) {
73
+ if (!options.apiKey) {
74
+ return res.status(404).json({ message: 'Not found' });
75
+ }
76
+ if (cors(req, res)) {
77
+ return;
78
+ }
79
+ if (req.method !== 'GET') {
80
+ return res.status(405).end();
81
+ }
82
+ try {
83
+ await workspace();
84
+ await registerResource();
85
+ res.setHeader('Cache-Control', 'no-store');
86
+ return res.status(200).json(config.metadata);
87
+ } catch (err) {
88
+ return res.status(500).json({ message: err.message });
89
+ }
90
+ }
91
+
92
+ async function endpoint(req, res) {
93
+ if (cors(req, res)) {
94
+ return;
95
+ }
96
+ if (!options.apiKey) {
97
+ return mcp(backend)(req, res);
98
+ }
99
+ try {
100
+ const currentWorkspace = await workspace();
101
+ const token = `${req.headers.authorization || ''}`.replace(/^Bearer\s+/i, '').trim();
102
+ if (!token) {
103
+ await registerResource();
104
+ challenge(res);
105
+ return res.status(401).json({ message: 'Not authorized' });
106
+ }
107
+
108
+ let user;
109
+ let roles;
110
+ const context = { $workspaceId: currentWorkspace._id };
111
+ if (token.startsWith('mcp_at_')) {
112
+ const result = await mothership.MCPOAuthToken.introspectMCPAccessToken({
113
+ apiKey: options.apiKey, token, resource
114
+ }, options);
115
+ user = result.user;
116
+ roles = result.permissions.roles;
117
+ context.maxTimeMS = result.permissions.maxTimeMS;
118
+ context.readPreference = result.permissions.readPreference;
119
+ } else {
120
+ const response = await fetch(`${mothershipUrl}/me`, {
121
+ method: 'POST',
122
+ body: JSON.stringify({ workspaceId: currentWorkspace._id }),
123
+ headers: { Authorization: token, 'Content-Type': 'application/json' }
124
+ });
125
+ if (!response.ok) {
126
+ return res.status(response.status).json({ message: 'Not authorized' });
127
+ }
128
+ ({ user, roles } = await response.json());
129
+ }
130
+ if (!user || !roles) {
131
+ return res.status(403).json({ message: 'Not authorized' });
132
+ }
133
+ req._internals = {
134
+ ...context,
135
+ authorization: token,
136
+ initiatedById: user._id,
137
+ initiatedBy: user,
138
+ userId: user._id,
139
+ roles
140
+ };
141
+ return mcp(backend)(req, res);
142
+ } catch (err) {
143
+ if (err.status === 401 || err.status === 403) {
144
+ challenge(res, err.message);
145
+ return res.status(401).json({ message: err.message });
146
+ }
147
+ return res.status(500).json({ message: err.message });
148
+ }
149
+ }
150
+
151
+ return { endpoint, metadata };
152
+ };
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ const Backend = require('../');
4
+ const checkVercelBindIp = require('./checkBindIp');
5
+ const vercelMCP = require('./vercelMCP');
6
+
7
+ module.exports = function vercelMCPRouteHandler(conn, options) {
8
+ options = { ...options, mcp: true, _mcpPath: '/api/studio-mcp' };
9
+ const checkRequest = checkVercelBindIp(options);
10
+ const backend = Backend(conn, options.studioConnection, options);
11
+ const { endpoint } = vercelMCP(conn, options, backend);
12
+
13
+ return function wrappedVercelMCPRouteHandler(req, res) {
14
+ if (!checkRequest(req, res)) {
15
+ return;
16
+ }
17
+ return endpoint(req, res);
18
+ };
19
+ };
package/docs/mcp-oauth.md CHANGED
@@ -24,7 +24,7 @@ It publishes metadata that tells clients where the authorization server is, and
24
24
  8. On every MCP request, Studio asks the authorization server to resolve the access token, and runs the request under the policy it returns.
25
25
 
26
26
  Authorization Code with PKCE (`S256`) is required, and there are no client secrets: MCP clients are public clients.
27
- Access tokens live 15 minutes.
27
+ Access tokens live 24 hours.
28
28
  Refresh tokens live 30 days and rotate on every use, so a user does not have to reconnect the client regularly.
29
29
 
30
30
  ## Authorization grants
package/express.js CHANGED
@@ -21,7 +21,7 @@ module.exports = async function mongooseStudioExpressApp(apiUrl, conn, options)
21
21
  if (options.mcp && options.apiKey && !options.publicUrl) {
22
22
  throw new Error('Mongoose Studio requires the publicUrl option when MCP is enabled with an API key');
23
23
  }
24
- const hasBindIpOption = Object.prototype.hasOwnProperty.call(options, 'bindIp');
24
+ const hasBindIpOption = options.bindIp != null;
25
25
  const bindIp = normalizeBindIPOption(options.bindIp);
26
26
 
27
27
  const mothershipUrl = options._mothershipUrl || defaultMothershipURL;
package/frontend/index.js CHANGED
@@ -5,6 +5,8 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
  const webpack = require('webpack');
7
7
  const { defaultMothershipURL } = require('../constants');
8
+ const mothership = require('../backend/integrations/mothership');
9
+ const vercelMCPConfig = require('../backend/vercel/mcpConfig');
8
10
  const webpackConfig = require('./webpack.config');
9
11
  const util = require('util');
10
12
 
@@ -33,11 +35,18 @@ module.exports = async function frontend(apiUrl, isLambda, options, workspace) {
33
35
 
34
36
  const { apiKey, ...workspaceData } = workspace || {};
35
37
  const workspaceBaseUrl = workspace?.baseUrl?.replace(/\/+$/, '');
38
+ const publicBaseUrl = options?.publicUrl?.replace(/\/+$/, '') || workspaceBaseUrl;
39
+ const mcpPath = options?._mcpPath || (isLambda && options?.mcp === true ? '/api/studio-mcp' : null);
40
+ const mcpEnabled = options?.mcp ?? !options?.apiKey;
36
41
  const config = {
37
42
  baseURL: apiUrl,
38
43
  isLambda,
39
- mcp: options?.mcp !== false,
40
- mcpUrl: options?.mcp !== false && workspaceBaseUrl ? `${workspaceBaseUrl}/mcp` : null,
44
+ mcp: mcpEnabled,
45
+ mcpUrl: mcpEnabled
46
+ ? mcpPath
47
+ ? publicBaseUrl ? new URL(mcpPath, `${publicBaseUrl}/`).toString() : mcpPath
48
+ : publicBaseUrl ? `${publicBaseUrl}/mcp` : null
49
+ : null,
41
50
  mothershipUrl: apiKey ? mothershipUrl : null,
42
51
  workspace: workspaceData
43
52
  };
@@ -45,6 +54,19 @@ module.exports = async function frontend(apiUrl, isLambda, options, workspace) {
45
54
  if (isLambda) {
46
55
  const configPath = path.join(__dirname, './public/config.js');
47
56
  fs.writeFileSync(configPath, `window.MONGOOSE_STUDIO_CONFIG = ${JSON.stringify(config, null, 2)};`);
57
+
58
+ // The caller copies frontend/public after this promise resolves. Put the
59
+ // protected resource document there so the same copy includes it.
60
+ const metadataPath = path.join(__dirname, 'public/.well-known/oauth-protected-resource');
61
+ if (options?.apiKey && options?.mcp === true) {
62
+ fs.rmSync(metadataPath, { force: true });
63
+ const mcpConfig = vercelMCPConfig({ ...options, _mcpPath: '/api/studio-mcp' });
64
+ await mothership.Workspace.registerMCPResource({ apiKey: options.apiKey, resource: mcpConfig.resource }, options);
65
+ fs.mkdirSync(path.dirname(metadataPath), { recursive: true });
66
+ fs.writeFileSync(metadataPath, JSON.stringify(mcpConfig.metadata, null, 2));
67
+ } else {
68
+ fs.rmSync(metadataPath, { force: true });
69
+ }
48
70
  }
49
71
 
50
72
  if (options && options.__watch) {