@ferrlabs/mcp-core 6.0.0 → 6.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.
@@ -2,6 +2,12 @@ interface RequestOptions {
2
2
  method?: string;
3
3
  body?: unknown;
4
4
  token?: string;
5
+ /**
6
+ * Override the API base URL for this call. Sub-MCPs targeting per-product
7
+ * APIs (e.g. `api.ferrgrowth.com`, `api.ferrfleet.com`) set this on each
8
+ * tool call. Falls back to `API_URL` env var, then `api.ferrlabs.com`.
9
+ */
10
+ baseUrl?: string;
5
11
  }
6
12
  export declare function apiRequest<T>(path: string, options?: RequestOptions): Promise<T>;
7
13
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AAEA,UAAU,cAAc;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,UAAU,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,CAAC,CAAC,CA0C1F"}
1
+ {"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AAEA,UAAU,cAAc;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAsB,UAAU,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,CAAC,CAAC,CA2C1F"}
@@ -1,6 +1,7 @@
1
- const API_URL = process.env.API_URL ?? 'https://api.ferrlabs.com';
1
+ const DEFAULT_API_URL = process.env.API_URL ?? 'https://api.ferrlabs.com';
2
2
  export async function apiRequest(path, options = {}) {
3
- const { method = 'GET', body, token } = options;
3
+ const { method = 'GET', body, token, baseUrl } = options;
4
+ const base = baseUrl ?? DEFAULT_API_URL;
4
5
  const headers = {
5
6
  'Content-Type': 'application/json',
6
7
  'User-Agent': 'ferrlabs-mcp/4.0.0',
@@ -9,7 +10,7 @@ export async function apiRequest(path, options = {}) {
9
10
  headers['x-api-token'] = token;
10
11
  headers['Authorization'] = `Bearer ${token}`;
11
12
  }
12
- const res = await fetch(`${API_URL}${path}`, {
13
+ const res = await fetch(`${base}${path}`, {
13
14
  method,
14
15
  headers,
15
16
  body: body ? JSON.stringify(body) : undefined,
@@ -4,6 +4,13 @@ export interface HttpServerOptions {
4
4
  host?: string;
5
5
  stateless?: boolean;
6
6
  createServer: () => McpServer;
7
+ /**
8
+ * URL of the OAuth 2.0 Authorization Server users authenticate against.
9
+ * Surfaced to MCP clients via /.well-known/oauth-protected-resource so
10
+ * they can drive the standard PKCE flow when no Bearer is present.
11
+ * Defaults to `FERRLABS_AUTH_URL` env or `https://auth.ferrlabs.com`.
12
+ */
13
+ authorizationServer?: string;
7
14
  }
8
15
  export declare function startHttpServer(opts: HttpServerOptions): Promise<void>;
9
16
  //# sourceMappingURL=http.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/transports/http.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAUzE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,YAAY,EAAE,MAAM,SAAS,CAAC;CAC/B;AAoBD,wBAAsB,eAAe,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA2E5E"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/transports/http.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAiBzE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,YAAY,EAAE,MAAM,SAAS,CAAC;IAC9B;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAoBD,wBAAsB,eAAe,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuH5E"}
@@ -9,6 +9,11 @@ function extractBearer(req) {
9
9
  const match = /^Bearer\s+(.+)$/i.exec(header);
10
10
  return match?.[1];
11
11
  }
12
+ function publicOrigin(req) {
13
+ const proto = req.headers['x-forwarded-proto']?.split(',')[0]?.trim() ?? 'https';
14
+ const host = req.headers['x-forwarded-host'] ?? req.headers.host ?? '';
15
+ return `${proto}://${host}`;
16
+ }
12
17
  async function readJsonBody(req) {
13
18
  if (req.method !== 'POST')
14
19
  return undefined;
@@ -31,8 +36,40 @@ async function readJsonBody(req) {
31
36
  }
32
37
  export async function startHttpServer(opts) {
33
38
  const { port, host = '0.0.0.0', stateless = true } = opts;
39
+ const authServer = opts.authorizationServer ?? process.env.FERRLABS_AUTH_URL ?? 'https://auth.ferrlabs.com';
34
40
  const transports = new Map();
41
+ function sendUnauthorized(req, res) {
42
+ const origin = publicOrigin(req);
43
+ const metadataUrl = `${origin}/.well-known/oauth-protected-resource`;
44
+ res.writeHead(401, {
45
+ 'Content-Type': 'application/json',
46
+ 'WWW-Authenticate': `Bearer resource_metadata="${metadataUrl}"`,
47
+ });
48
+ res.end(JSON.stringify({
49
+ jsonrpc: '2.0',
50
+ error: {
51
+ code: -32001,
52
+ message: 'Missing or invalid bearer token. The MCP client should fetch the OAuth protected-resource metadata and run the authorization code + PKCE flow.',
53
+ },
54
+ id: null,
55
+ }));
56
+ }
57
+ function sendResourceMetadata(req, res) {
58
+ const origin = publicOrigin(req);
59
+ res.writeHead(200, { 'Content-Type': 'application/json' });
60
+ res.end(JSON.stringify({
61
+ resource: origin,
62
+ authorization_servers: [authServer],
63
+ bearer_methods_supported: ['header'],
64
+ resource_documentation: 'https://ferrlabs.com',
65
+ }));
66
+ }
35
67
  async function handleMcpRequest(req, res) {
68
+ const bearerToken = extractBearer(req);
69
+ if (!bearerToken) {
70
+ sendUnauthorized(req, res);
71
+ return;
72
+ }
36
73
  const sessionId = req.headers['mcp-session-id'] ?? undefined;
37
74
  let transport = sessionId ? transports.get(sessionId) : undefined;
38
75
  if (sessionId && !transport && !stateless) {
@@ -67,7 +104,6 @@ export async function startHttpServer(opts) {
67
104
  await server.connect(transport);
68
105
  }
69
106
  const body = await readJsonBody(req);
70
- const bearerToken = extractBearer(req);
71
107
  await runWithAuthContext({ bearerToken }, () => transport.handleRequest(req, res, body));
72
108
  }
73
109
  const httpServer = createHttpServer((req, res) => {
@@ -77,6 +113,10 @@ export async function startHttpServer(opts) {
77
113
  res.end(JSON.stringify({ status: 'ok', service: 'ferrlabs-mcp' }));
78
114
  return;
79
115
  }
116
+ if (url === '/.well-known/oauth-protected-resource') {
117
+ sendResourceMetadata(req, res);
118
+ return;
119
+ }
80
120
  if (url === '/mcp' || url.startsWith('/mcp?')) {
81
121
  handleMcpRequest(req, res).catch((err) => {
82
122
  if (!res.headersSent) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ferrlabs/mcp-core",
3
- "version": "6.0.0",
4
- "description": "Shared core for FerrLabs MCP servers — auth, transports (stdio + Streamable HTTP), api-client, runner",
3
+ "version": "6.2.0",
4
+ "description": "Shared core for FerrLabs MCP servers — auth, transports (stdio + Streamable HTTP), api-client, runner",
5
5
  "license": "MPL-2.0",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",