fix(mcp): advertise OAuth security metadata
This commit is contained in:
parent
478f90a230
commit
0120463332
@ -5,12 +5,14 @@ KEYCLOAK_JWKS_URI=https://auth.example.invalid/realms/example/protocol/openid-co
|
|||||||
KEYCLOAK_AUDIENCE=fieldlens-api
|
KEYCLOAK_AUDIENCE=fieldlens-api
|
||||||
KEYCLOAK_BROWSER_CLIENT_ID=fieldlens
|
KEYCLOAK_BROWSER_CLIENT_ID=fieldlens
|
||||||
KEYCLOAK_DESKTOP_CLIENT_ID=fieldlens-print
|
KEYCLOAK_DESKTOP_CLIENT_ID=fieldlens-print
|
||||||
|
KEYCLOAK_MCP_CLIENT_ID=fieldlens-mcp
|
||||||
KEYCLOAK_MACHINE_CLIENT_ID=fieldlens-integration
|
KEYCLOAK_MACHINE_CLIENT_ID=fieldlens-integration
|
||||||
FIELDLENS_TENANT_ID=fieldlens-production
|
FIELDLENS_TENANT_ID=fieldlens-production
|
||||||
FIELDLENS_ENV=development
|
FIELDLENS_ENV=development
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
WEB_ORIGIN=http://localhost:5173
|
WEB_ORIGIN=http://localhost:5173
|
||||||
STATIC_DIR=
|
STATIC_DIR=
|
||||||
|
FIELDLENS_MCP_RESOURCE_URL=http://localhost:5173/api/mcp
|
||||||
FIELDLENS_PRINT_RELEASE_TAG=fieldlens-print-v0.1.0
|
FIELDLENS_PRINT_RELEASE_TAG=fieldlens-print-v0.1.0
|
||||||
FIELDLENS_PRINT_RELEASE_BASE_URL=https://git.example.invalid/owner/fieldlens/releases/download
|
FIELDLENS_PRINT_RELEASE_BASE_URL=https://git.example.invalid/owner/fieldlens/releases/download
|
||||||
FIELDLENS_PRINT_DOWNLOAD_DIR=downloads
|
FIELDLENS_PRINT_DOWNLOAD_DIR=downloads
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||||
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
|
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
|
||||||
import { captureLimit, createFieldLensMcpServer } from './mcp.js'
|
import { FIELDLENS_MCP_OAUTH_SCOPES, captureLimit, createFieldLensMcpServer } from './mcp.js'
|
||||||
|
|
||||||
describe('FieldLens MCP telemetry', () => {
|
describe('FieldLens MCP telemetry', () => {
|
||||||
it('exposes only a bounded read-only diagnostic tool', async () => {
|
it('exposes only a bounded read-only diagnostic tool', async () => {
|
||||||
@ -14,6 +14,10 @@ describe('FieldLens MCP telemetry', () => {
|
|||||||
expect(tools.map((tool) => tool.name)).toEqual(['list_print_captures'])
|
expect(tools.map((tool) => tool.name)).toEqual(['list_print_captures'])
|
||||||
expect(tools[0]?.annotations?.readOnlyHint).toBe(true)
|
expect(tools[0]?.annotations?.readOnlyHint).toBe(true)
|
||||||
expect(tools[0]?.inputSchema.properties?.limit).toBeDefined()
|
expect(tools[0]?.inputSchema.properties?.limit).toBeDefined()
|
||||||
|
expect(tools[0]?.securitySchemes).toEqual([
|
||||||
|
{ type: 'oauth2', scopes: [...FIELDLENS_MCP_OAUTH_SCOPES] },
|
||||||
|
])
|
||||||
|
expect(tools[0]?._meta?.securitySchemes).toEqual(tools[0]?.securitySchemes)
|
||||||
} finally {
|
} finally {
|
||||||
await client.close()
|
await client.close()
|
||||||
await server.close()
|
await server.close()
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import { config } from './config.js'
|
|||||||
import { pool } from './db.js'
|
import { pool } from './db.js'
|
||||||
import { log } from './log.js'
|
import { log } from './log.js'
|
||||||
|
|
||||||
|
export const FIELDLENS_MCP_OAUTH_SCOPES = ['openid', 'offline_access'] as const
|
||||||
|
|
||||||
export function captureLimit(value: number) {
|
export function captureLimit(value: number) {
|
||||||
return Number.isFinite(value) ? Math.max(1, Math.min(200, Math.trunc(value))) : 50
|
return Number.isFinite(value) ? Math.max(1, Math.min(200, Math.trunc(value))) : 50
|
||||||
}
|
}
|
||||||
@ -16,6 +18,10 @@ export function createFieldLensMcpServer() {
|
|||||||
description: 'Read recent FieldLens printer capture diagnostics for the authorized tenant. No print document or barcode values are included.',
|
description: 'Read recent FieldLens printer capture diagnostics for the authorized tenant. No print document or barcode values are included.',
|
||||||
inputSchema: { limit: z.number().int().min(1).max(200).optional() },
|
inputSchema: { limit: z.number().int().min(1).max(200).optional() },
|
||||||
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
||||||
|
securitySchemes: [{ type: 'oauth2', scopes: [...FIELDLENS_MCP_OAUTH_SCOPES] }],
|
||||||
|
_meta: {
|
||||||
|
securitySchemes: [{ type: 'oauth2', scopes: [...FIELDLENS_MCP_OAUTH_SCOPES] }],
|
||||||
|
},
|
||||||
}, async ({ limit }) => {
|
}, async ({ limit }) => {
|
||||||
const result = await pool.query(`
|
const result = await pool.query(`
|
||||||
SELECT id, device_id AS "deviceId", source, stream_format AS "streamFormat",
|
SELECT id, device_id AS "deviceId", source, stream_format AS "streamFormat",
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { config } from './config.js'
|
|||||||
import { loadDeviceContext } from './context.js'
|
import { loadDeviceContext } from './context.js'
|
||||||
import { databaseReady, pool } from './db.js'
|
import { databaseReady, pool } from './db.js'
|
||||||
import { log } from './log.js'
|
import { log } from './log.js'
|
||||||
import { handleMcpRequest } from './mcp.js'
|
import { FIELDLENS_MCP_OAUTH_SCOPES, handleMcpRequest } from './mcp.js'
|
||||||
import { runMigrations } from './migrations.js'
|
import { runMigrations } from './migrations.js'
|
||||||
import { processSyncEvent } from './sync.js'
|
import { processSyncEvent } from './sync.js'
|
||||||
|
|
||||||
@ -103,11 +103,11 @@ app.get('/downloads/fieldlens-print-windows-x64-setup.exe.sig', (_req, res) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
app.get('/downloads/fieldlens-print-linux-x64.AppImage', (_req, res) => {
|
app.get('/downloads/fieldlens-print-linux-x64.AppImage', (_req, res) => {
|
||||||
redirectDesktopDownload(res, 'fieldlens-print-linux-x64.AppImage', config.linuxDesktopDownloadBaseUrl)
|
redirectDesktopDownload(res, 'fieldlens-print-linux-x64.AppImage')
|
||||||
})
|
})
|
||||||
|
|
||||||
app.get('/downloads/fieldlens-print-linux-x64.AppImage.sig', (_req, res) => {
|
app.get('/downloads/fieldlens-print-linux-x64.AppImage.sig', (_req, res) => {
|
||||||
redirectDesktopDownload(res, 'fieldlens-print-linux-x64.AppImage.sig', config.linuxDesktopDownloadBaseUrl)
|
redirectDesktopDownload(res, 'fieldlens-print-linux-x64.AppImage.sig')
|
||||||
})
|
})
|
||||||
|
|
||||||
// Backward-compatible aliases used by the first downloads page.
|
// Backward-compatible aliases used by the first downloads page.
|
||||||
@ -132,11 +132,19 @@ app.get('/api/health/ready', async (_req, res) => {
|
|||||||
const mcpResourceMetadataUrl = new URL('/.well-known/oauth-protected-resource/api/mcp', config.mcpResourceUrl).href
|
const mcpResourceMetadataUrl = new URL('/.well-known/oauth-protected-resource/api/mcp', config.mcpResourceUrl).href
|
||||||
app.get('/.well-known/oauth-protected-resource/api/mcp', (_req, res) => {
|
app.get('/.well-known/oauth-protected-resource/api/mcp', (_req, res) => {
|
||||||
res.setHeader('Cache-Control', 'public, max-age=300')
|
res.setHeader('Cache-Control', 'public, max-age=300')
|
||||||
res.json({ resource: config.mcpResourceUrl, authorization_servers: [config.issuer], bearer_methods_supported: ['header'] })
|
res.json({
|
||||||
|
resource: config.mcpResourceUrl,
|
||||||
|
authorization_servers: [config.issuer],
|
||||||
|
scopes_supported: [...FIELDLENS_MCP_OAUTH_SCOPES],
|
||||||
|
bearer_methods_supported: ['header'],
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.use('/api/mcp', (_req, res, next) => {
|
app.use('/api/mcp', (_req, res, next) => {
|
||||||
res.setHeader('WWW-Authenticate', `Bearer resource_metadata="${mcpResourceMetadataUrl}"`)
|
res.setHeader(
|
||||||
|
'WWW-Authenticate',
|
||||||
|
`Bearer resource_metadata="${mcpResourceMetadataUrl}", scope="${FIELDLENS_MCP_OAUTH_SCOPES.join(' ')}"`,
|
||||||
|
)
|
||||||
res.setHeader('Cache-Control', 'no-store')
|
res.setHeader('Cache-Control', 'no-store')
|
||||||
next()
|
next()
|
||||||
}, authenticate, (_req, res, next) => {
|
}, authenticate, (_req, res, next) => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user