@ferrlabs/mcp-core 6.4.0 → 6.5.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.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=http.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.test.d.ts","sourceRoot":"","sources":["../../../src/transports/__tests__/http.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,24 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { unauthorizedChallenge } from '../http.js';
3
+ describe('unauthorizedChallenge', () => {
4
+ const metadataUrl = 'https://mcp.ferrvault.com/.well-known/oauth-protected-resource';
5
+ it('returns an RFC 6749 §5.2 error body where `error` is a string', () => {
6
+ const { body } = unauthorizedChallenge(metadataUrl);
7
+ const parsed = JSON.parse(body);
8
+ expect(typeof parsed.error).toBe('string');
9
+ expect(parsed.error).toBe('invalid_token');
10
+ expect(typeof parsed.error_description).toBe('string');
11
+ });
12
+ it('does not emit a JSON-RPC envelope that strict OAuth clients reject', () => {
13
+ const { body } = unauthorizedChallenge(metadataUrl);
14
+ const parsed = JSON.parse(body);
15
+ expect(parsed.jsonrpc).toBeUndefined();
16
+ expect(typeof parsed.error).not.toBe('object');
17
+ });
18
+ it('builds a Bearer challenge carrying the error code and the resource metadata URL', () => {
19
+ const { wwwAuthenticate } = unauthorizedChallenge(metadataUrl);
20
+ expect(wwwAuthenticate.startsWith('Bearer ')).toBe(true);
21
+ expect(wwwAuthenticate).toContain('error="invalid_token"');
22
+ expect(wwwAuthenticate).toContain(`resource_metadata="${metadataUrl}"`);
23
+ });
24
+ });
@@ -1,4 +1,8 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function unauthorizedChallenge(metadataUrl: string): {
3
+ wwwAuthenticate: string;
4
+ body: string;
5
+ };
2
6
  export interface HttpServerOptions {
3
7
  port: number;
4
8
  host?: string;
@@ -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;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;;;;;;;;;;;;OAYG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAoBD,wBAAsB,eAAe,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6I5E"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/transports/http.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAoBzE,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG;IAC1D,eAAe,EAAE,MAAM,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;CACd,CAQA;AAED,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;;;;;;;;;;;;OAYG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAoBD,wBAAsB,eAAe,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqI5E"}
@@ -14,6 +14,16 @@ function publicOrigin(req) {
14
14
  const host = req.headers['x-forwarded-host'] ?? req.headers.host ?? '';
15
15
  return `${proto}://${host}`;
16
16
  }
17
+ const UNAUTHORIZED_DESCRIPTION = 'Missing or invalid bearer token. Fetch the OAuth protected-resource metadata and run the authorization code + PKCE flow.';
18
+ export function unauthorizedChallenge(metadataUrl) {
19
+ return {
20
+ wwwAuthenticate: `Bearer error="invalid_token", error_description="${UNAUTHORIZED_DESCRIPTION}", resource_metadata="${metadataUrl}"`,
21
+ body: JSON.stringify({
22
+ error: 'invalid_token',
23
+ error_description: UNAUTHORIZED_DESCRIPTION,
24
+ }),
25
+ };
26
+ }
17
27
  async function readJsonBody(req) {
18
28
  if (req.method !== 'POST')
19
29
  return undefined;
@@ -40,19 +50,12 @@ export async function startHttpServer(opts) {
40
50
  const transports = new Map();
41
51
  function sendUnauthorized(req, res) {
42
52
  const origin = publicOrigin(req);
43
- const metadataUrl = `${origin}/.well-known/oauth-protected-resource`;
53
+ const { wwwAuthenticate, body } = unauthorizedChallenge(`${origin}/.well-known/oauth-protected-resource`);
44
54
  res.writeHead(401, {
45
55
  'Content-Type': 'application/json',
46
- 'WWW-Authenticate': `Bearer resource_metadata="${metadataUrl}"`,
56
+ 'WWW-Authenticate': wwwAuthenticate,
47
57
  });
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
- }));
58
+ res.end(body);
56
59
  }
57
60
  function sendResourceMetadata(req, res) {
58
61
  const origin = publicOrigin(req);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ferrlabs/mcp-core",
3
- "version": "6.4.0",
3
+ "version": "6.5.0",
4
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",