diff --git a/apps/web/src/lib/server/mcp/__tests__/handler.test.ts b/apps/web/src/lib/server/mcp/__tests__/handler.test.ts index 27dae6b29..d56bbdcb5 100644 --- a/apps/web/src/lib/server/mcp/__tests__/handler.test.ts +++ b/apps/web/src/lib/server/mcp/__tests__/handler.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' import type { ApiKey } from '@/lib/server/domains/api-keys' +import type { PostListItem } from '@/lib/server/domains/posts/post.types' import type { PrincipalId, ApiKeyId, UserId } from '@opencoven-feedback/ids' // ── Mocks ────────────────────────────────────────────────────────────────────── @@ -904,6 +905,65 @@ describe('MCP HTTP Handler', () => { expect(text.comments).toEqual([]) }) + it('should exclude private comments from OAuth portal user post details', async () => { + const { getDeveloperConfig } = await import('@/lib/server/domains/settings/settings.service') + const { getCommentsWithReplies } = await import('@/lib/server/domains/posts/post.query') + vi.mocked(getDeveloperConfig).mockResolvedValue({ + mcpEnabled: true, + mcpPortalAccessEnabled: true, + }) + await setupValidOAuth({ role: 'user', scopes: ['read:feedback'] }) + + const { handleMcpRequest } = await import('../handler') + await handleMcpRequest( + oauthRequest( + jsonRpcRequest('initialize', { + protocolVersion: '2025-03-26', + capabilities: {}, + clientInfo: { name: 'test', version: '1.0' }, + }) + ) + ) + await setupValidOAuth({ role: 'user', scopes: ['read:feedback'] }) + vi.mocked(getCommentsWithReplies).mockClear() + + const response = await handleMcpRequest( + oauthRequest( + jsonRpcRequest('tools/call', { + name: 'get_details', + arguments: { id: 'post_test' }, + }) + ) + ) + + expect(response.status).toBe(200) + expect(vi.mocked(getCommentsWithReplies)).toHaveBeenCalledTimes(1) + expect(vi.mocked(getCommentsWithReplies)).toHaveBeenLastCalledWith('post_test', undefined, { + publicOnly: true, + }) + }) + + it('should include private comments in team member post details', async () => { + const { getCommentsWithReplies } = await import('@/lib/server/domains/posts/post.query') + const handleMcpRequest = await initializeSession() + vi.mocked(getCommentsWithReplies).mockClear() + + const response = await handleMcpRequest( + mcpRequest( + jsonRpcRequest('tools/call', { + name: 'get_details', + arguments: { id: 'post_test' }, + }) + ) + ) + + expect(response.status).toBe(200) + expect(vi.mocked(getCommentsWithReplies)).toHaveBeenCalledTimes(1) + expect(vi.mocked(getCommentsWithReplies)).toHaveBeenLastCalledWith('post_test', undefined, { + publicOnly: false, + }) + }) + // ── get_details tool (changelog) ──────────────────────────────────────── it('should handle tools/call for get_details with changelog TypeID', async () => { @@ -1674,7 +1734,7 @@ describe('MCP HTTP Handler', () => { voteCount: 1, commentCount: 1, boardId: 'board_test', - board: { name: 'Feedback' }, + board: { id: 'board_test', name: 'Feedback', slug: 'feedback' }, statusId: 'status_test', authorName: 'Portal User', ownerPrincipalId: null, @@ -1684,7 +1744,7 @@ describe('MCP HTTP Handler', () => { isCommentsLocked: false, createdAt: new Date('2026-01-01'), deletedAt: null, - }, + } as unknown as PostListItem, ], nextCursor: null, hasMore: false, diff --git a/apps/web/src/lib/server/mcp/tools.ts b/apps/web/src/lib/server/mcp/tools.ts index e834a7e09..b18efbaae 100644 --- a/apps/web/src/lib/server/mcp/tools.ts +++ b/apps/web/src/lib/server/mcp/tools.ts @@ -2043,14 +2043,14 @@ async function searchArticles(args: SearchArgs): Promise { // ============================================================================ async function getPostDetails(postId: PostId, auth: McpAuthContext): Promise { + const includeTeamOnlyFields = isTeamMember(auth.role) + const [post, comments, mergedPosts] = await Promise.all([ getPostWithDetails(postId), - getCommentsWithReplies(postId), + getCommentsWithReplies(postId, undefined, { publicOnly: !includeTeamOnlyFields }), getMergedPosts(postId), ]) - const includeTeamOnlyFields = isTeamMember(auth.role) - return jsonResult({ id: post.id, title: post.title,