Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 0 additions & 41 deletions .github/UPDATES_FORMAT.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,47 +62,6 @@ One update per release for all synchronized packages:
- Focus on benefits and practical capabilities
- NO changelog link at end (href already points to releases)

#### B. Independent Libraries

> **Note:** Independent libraries (`json-schema-to-zod-v3`, `mcp-from-openapi`, etc.) have been moved to external repositories. This section is kept for historical reference only. New releases no longer include independent library updates.

Separate update per independent library published in this release:

```mdx
<Update label="my-library v1.2.0" description="2025-11-22" tags={["Independent"]}>
<Card
title="my-library v1.2.0"
href="https://github.com/org/my-library"
cta="Explore the library"
>
🔒 **Feature 1** – Description of what this feature does for users.

🧠 **Feature 2** – Another benefit-focused description.

🛠️ **Feature 3** – How this improves the user experience.

</Card>
</Update>
```

**Fields:**

- `label`: `"{package-name} v{version}"` (e.g., `"my-library v1.2.0"`)
- `description`: ISO date format `"YYYY-MM-DD"` (same as FrontMCP release)
- `tags`: `{["Independent"]}`
- `title`: `"{package-name} v{version}"`
- `href`: Link to the library's repository
- `cta`: `"Explore the library"`

**Content format:**

- Use emoji at start of each line
- Format: `emoji **Bold feature name** – Description.`
- Use en dash (–) not hyphen (-)
- Each feature on its own line with blank line between
- Focus on practical use cases
- NO changelog link at end

## Complete Example

### Live Updates (docs/live/updates.mdx)
Expand Down
4 changes: 2 additions & 2 deletions apps/demo/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ module.exports = {
compiler: 'tsc',
main: './src/main.ts',
sourceMap: true,
mergeExternals: true,
tsConfig: './tsconfig.app.json',
assets: [],
externalDependencies: 'all',
optimization: false,
outputHashing: 'none',
generatePackageJson: false,
buildLibsFromSource: true,
buildLibsFromSource: false,
}),
],
};
223 changes: 223 additions & 0 deletions apps/e2e/demo-e2e-openapi/e2e/openapi-security.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
/**
* E2E Tests for OpenAPI Adapter Security
*
* Tests that staticAuth and other auth mechanisms correctly send
* Authorization headers to the backend API.
*
* These tests use MockAPIServer with handler functions to capture
* and verify the actual headers received by the mock API.
*/
import { TestServer, MockAPIServer, McpTestClient, expect, MockRequest, MockResponseHelper } from '@frontmcp/testing';

// OpenAPI spec with bearer auth
const SECURED_OPENAPI_SPEC = {
openapi: '3.0.0',
info: { title: 'Secured API', version: '1.0.0' },
components: {
securitySchemes: {
BearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
},
security: [{ BearerAuth: [] }],
paths: {
'/secured-endpoint': {
get: {
operationId: 'getSecuredData',
summary: 'Get secured data (requires auth)',
security: [{ BearerAuth: [] }],
responses: {
'200': {
description: 'Success',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
message: { type: 'string' },
receivedAuth: { type: 'string' },
},
},
},
},
},
'401': { description: 'Unauthorized' },
},
},
},
'/public-endpoint': {
get: {
operationId: 'getPublicData',
summary: 'Get public data (no auth required)',
security: [],
responses: {
'200': {
description: 'Success',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
message: { type: 'string' },
},
},
},
},
},
},
},
},
},
};

describe('OpenAPI Adapter Security E2E', () => {
// Track received headers for verification (scoped to this suite for isolation)
let lastReceivedHeaders: Record<string, string | undefined> = {};
let mockApi: MockAPIServer;
let server: TestServer;
let client: McpTestClient;

beforeAll(async () => {
// Reset captured headers
lastReceivedHeaders = {};

// Create mock API server with handler to capture headers
mockApi = new MockAPIServer({
openApiSpec: SECURED_OPENAPI_SPEC,
routes: [
{
method: 'GET',
path: '/secured-endpoint',
handler: (req: MockRequest, res: MockResponseHelper) => {
// Capture headers for verification
lastReceivedHeaders = { ...req.headers };

const authHeader = req.headers['authorization'];
if (!authHeader || !authHeader.startsWith('Bearer ')) {
res.json({ error: 'Unauthorized', message: 'Missing or invalid Authorization header' }, 401);
return;
}

res.json({
message: 'Access granted',
receivedAuth: authHeader,
});
},
},
{
method: 'GET',
path: '/public-endpoint',
handler: (req: MockRequest, res: MockResponseHelper) => {
// Capture headers for public endpoint too
lastReceivedHeaders = { ...req.headers };

res.json({
message: 'Public data',
hasAuth: !!req.headers['authorization'],
});
},
},
],
debug: false,
});
const apiInfo = await mockApi.start();

// Start MCP server with staticAuth configuration
server = await TestServer.start({
command: 'npx tsx apps/e2e/demo-e2e-openapi/src/security-test-main.ts',
env: {
OPENAPI_BASE_URL: apiInfo.baseUrl,
OPENAPI_SPEC_URL: apiInfo.specUrl,
STATIC_AUTH_JWT: 'test-jwt-token-12345',
},
startupTimeout: 30000,
debug: false,
});

client = await McpTestClient.create({
baseUrl: server.info.baseUrl,
transport: 'streamable-http',
publicMode: true,
}).buildAndConnect();
}, 60000);

afterAll(async () => {
if (client) await client.disconnect();
if (server) await server.stop();
if (mockApi) await mockApi.stop();
});

beforeEach(() => {
// Reset captured headers before each test
lastReceivedHeaders = {};
});

describe('Tool Generation with Security', () => {
it('should generate tools from secured OpenAPI spec', async () => {
const tools = await client.tools.list();

expect(tools).toBeDefined();
expect(tools.length).toBeGreaterThan(0);

// Verify secured tool exists
const securedTool = tools.find((t) => t.name === 'getSecuredData');
expect(securedTool).toBeDefined();

// Verify public tool exists
const publicTool = tools.find((t) => t.name === 'getPublicData');
expect(publicTool).toBeDefined();
});
});

describe('staticAuth JWT', () => {
it('should include Authorization Bearer header in API requests', async () => {
// Call the secured tool
const result = await client.tools.call('getSecuredData', {});

// Verify the response indicates success (auth was accepted)
expect(result).toBeDefined();
expect(result.isSuccess).toBe(true);

// Parse the JSON response - OpenAPI adapter wraps in { data: { ... } }
const response = result.json<{ data: { message: string; receivedAuth: string } }>();
expect(response.data.message).toBe('Access granted');
expect(response.data.receivedAuth).toBe('Bearer test-jwt-token-12345');
});

it('should have sent Authorization header to mock server', async () => {
// Call the tool to populate lastReceivedHeaders
await client.tools.call('getSecuredData', {});

// Verify the header was actually received by the mock server
expect(lastReceivedHeaders['authorization']).toBe('Bearer test-jwt-token-12345');
});

it('should include accept header in requests', async () => {
await client.tools.call('getSecuredData', {});

// Verify accept header is set
expect(lastReceivedHeaders['accept']).toBe('application/json');
});
});

describe('Public Endpoints', () => {
it('should NOT include auth header for public endpoints (security: [])', async () => {
// Call the public tool (which has security: [] in OpenAPI spec)
const result = await client.tools.call('getPublicData', {});

expect(result).toBeDefined();
expect(result.isSuccess).toBe(true);

// Parse the JSON response - OpenAPI adapter wraps in { data: { ... } }
const response = result.json<{ data: { message: string; hasAuth: boolean } }>();
expect(response.data.message).toBe('Public data');

// Public endpoints with security: [] should NOT receive auth
// This is correct behavior - auth is only sent for endpoints that require it
expect(response.data.hasAuth).toBe(false);
});
});
});
49 changes: 49 additions & 0 deletions apps/e2e/demo-e2e-openapi/src/security-test-main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Security Test Entry Point for OpenAPI Adapter E2E Tests
*
* This server is configured with staticAuth to test that Authorization headers
* are correctly sent to backend APIs.
*/
import { FrontMcp, App, LogLevel } from '@frontmcp/sdk';
import { OpenapiAdapter } from '@frontmcp/adapters';

const port = parseInt(process.env['PORT'] ?? '3015', 10);
const apiBaseUrl = process.env['OPENAPI_BASE_URL'] || 'http://localhost:3000';
const openapiUrl = process.env['OPENAPI_SPEC_URL'] || `${apiBaseUrl}/openapi.json`;
const staticJwt = process.env['STATIC_AUTH_JWT'];

@App({
name: 'Security-Test',
description: 'Security test app for OpenAPI adapter with staticAuth',
adapters: [
OpenapiAdapter.init({
name: 'secured-api',
url: openapiUrl,
baseUrl: apiBaseUrl,
// Use staticAuth with JWT from environment variable
staticAuth: staticJwt ? { jwt: staticJwt } : undefined,
}),
],
})
class SecurityTestApp {}

@FrontMcp({
info: { name: 'Demo E2E OpenAPI Security', version: '0.1.0' },
apps: [SecurityTestApp],
logging: { level: LogLevel.Verbose },
http: { port },
auth: {
mode: 'public',
sessionTtl: 3600,
anonymousScopes: ['anonymous'],
transport: {
enableStatefulHttp: true,
enableStreamableHttp: true,
enableStatelessHttp: false,
requireSessionForStreamable: false,
enableLegacySSE: true,
enableSseListener: true,
},
},
})
export default class Server {}
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default [
...nx.configs['flat/typescript'],
...nx.configs['flat/javascript'],
{
ignores: ['**/dist'],
ignores: ['**/dist', '**/*.d.ts', '**/*.d.ts.map'],
},
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
Expand Down
Loading
Loading