Skip to content

Commit 5270b84

Browse files
silasjmatsonclaude
andcommitted
feat: add MCP redaction filtering for sensitive data
Add a secure-by-default redaction system that filters sensitive data from all MCP resource and tool responses. Uses a two-key model where default rules apply automatically and disabling requires agreement from both the client app and the Reactotron desktop app. Redaction engine: - Key matching: redacts object keys matching sensitiveKeys (case-insensitive) - Header matching: redacts HTTP headers matching headerNames - Value patterns: regex-based detection of Bearer tokens, JWTs, API keys - State path patterns: dot-separated paths with wildcard support - URL query params: redacts query param values matching sensitiveKeys - Handles circular references, nested objects, and arrays Client apps can send additionalRules (always allowed) or removeRules/ disableRedaction (requires server permission via settings modal). Desktop app: settings modal for editing rules and client permissions, redaction status indicator (shield icon) in footer, config persisted via electron-store. 44 tests (30 unit + 14 integration). Docs updated. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9dcedf2 commit 5270b84

20 files changed

Lines changed: 1646 additions & 61 deletions

File tree

apps/reactotron-app/src/renderer/RootModals.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {
55
ReactotronContext,
66
StateContext,
77
} from "reactotron-core-ui"
8+
import StandaloneContext from "./contexts/Standalone"
9+
import McpSettingsModal from "./components/McpSettingsModal"
810

911
function RootModals() {
1012
const {
@@ -19,6 +21,8 @@ function RootModals() {
1921
closeSubscriptionModal,
2022
} = useContext(ReactotronContext)
2123
const { addSubscription } = useContext(StateContext)
24+
const { mcpSettingsOpen, closeMcpSettings, mcpRedactionConfig, updateMcpRedactionConfig } =
25+
useContext(StandaloneContext)
2226

2327
const dispatchAction = (action: any) => {
2428
sendCommand("state.action.dispatch", { action })
@@ -46,6 +50,12 @@ function RootModals() {
4650
addSubscription(path)
4751
}}
4852
/>
53+
<McpSettingsModal
54+
isOpen={mcpSettingsOpen}
55+
onClose={closeMcpSettings}
56+
config={mcpRedactionConfig}
57+
onUpdate={updateMcpRedactionConfig}
58+
/>
4959
</>
5060
)
5161
}

apps/reactotron-app/src/renderer/components/Footer/Footer.story.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ storiesOf("components/Footer", module)
6666
mcpStatus="stopped"
6767
mcpPort={null}
6868
onToggleMcp={() => {}}
69+
mcpRedactionEnabled
70+
onOpenMcpSettings={() => {}}
6971
/>
7072
))
7173
.add("Collpased w/ connections", () => (
@@ -79,6 +81,8 @@ storiesOf("components/Footer", module)
7981
mcpStatus="stopped"
8082
mcpPort={null}
8183
onToggleMcp={() => {}}
84+
mcpRedactionEnabled
85+
onOpenMcpSettings={() => {}}
8286
/>
8387
))
8488
.add("Expanded", () => (
@@ -92,6 +96,8 @@ storiesOf("components/Footer", module)
9296
mcpStatus="stopped"
9397
mcpPort={null}
9498
onToggleMcp={() => {}}
99+
mcpRedactionEnabled
100+
onOpenMcpSettings={() => {}}
95101
/>
96102
))
97103
.add("Expanded w/ connections", () => (
@@ -105,6 +111,8 @@ storiesOf("components/Footer", module)
105111
mcpStatus="started"
106112
mcpPort={4567}
107113
onToggleMcp={() => {}}
114+
mcpRedactionEnabled
115+
onOpenMcpSettings={() => {}}
108116
/>
109117
))
110118
.add("Expanded w/ lots connections", () => (
@@ -118,5 +126,7 @@ storiesOf("components/Footer", module)
118126
mcpStatus="started"
119127
mcpPort={4567}
120128
onToggleMcp={() => {}}
129+
mcpRedactionEnabled
130+
onOpenMcpSettings={() => {}}
121131
/>
122132
))

apps/reactotron-app/src/renderer/components/Footer/Stateless.tsx

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from "react"
22
import styled from "styled-components"
3-
import { MdSwapVert as ExpandIcon } from "react-icons/md"
3+
import { MdSwapVert as ExpandIcon, MdSettings as SettingsIcon, MdShield as ShieldIcon } from "react-icons/md"
44

55
import config from "../../config"
66
import {
@@ -54,6 +54,12 @@ interface McpButtonProps {
5454
$active: boolean
5555
}
5656

57+
const McpGroup = styled.div`
58+
display: flex;
59+
align-items: center;
60+
gap: 2px;
61+
`
62+
5763
const McpButton = styled.div.attrs(() => ({}))<McpButtonProps>`
5864
display: flex;
5965
align-items: center;
@@ -78,6 +84,25 @@ const McpDot = styled.div<McpButtonProps>`
7884
background-color: ${(props) => props.$active ? "#50c878" : props.theme.foregroundDark};
7985
`
8086

87+
const McpSettingsButton = styled.div`
88+
display: flex;
89+
align-items: center;
90+
cursor: pointer;
91+
padding: 2px 4px;
92+
border-radius: 3px;
93+
color: ${(props) => props.theme.foregroundDark};
94+
&:hover {
95+
color: ${(props) => props.theme.foreground};
96+
background-color: rgba(255,255,255,0.05);
97+
}
98+
`
99+
100+
const RedactionBadge = styled.span<{ $warning?: boolean }>`
101+
display: flex;
102+
align-items: center;
103+
color: ${(props) => props.$warning ? "#e8a838" : "inherit"};
104+
`
105+
81106
function renderExpanded(
82107
serverStatus: ServerStatus,
83108
connections: Connection[],
@@ -137,6 +162,8 @@ interface Props {
137162
mcpStatus: McpStatus
138163
mcpPort: number | null
139164
onToggleMcp: () => void
165+
mcpRedactionEnabled: boolean
166+
onOpenMcpSettings: () => void
140167
}
141168

142169
function Header({
@@ -149,21 +176,38 @@ function Header({
149176
mcpStatus,
150177
mcpPort,
151178
onToggleMcp,
179+
mcpRedactionEnabled,
180+
onOpenMcpSettings,
152181
}: Props) {
153182
const renderMethod = isOpen ? renderExpanded : renderCollapsed
154183

155184
return (
156185
<Container>
157186
<ContentContainer onClick={() => !isOpen && setIsOpen(true)} $isOpen={isOpen}>
158187
{renderMethod(serverStatus, connections, selectedConnection, onChangeConnection)}
159-
<McpButton
160-
$active={mcpStatus === "started"}
161-
onClick={(e) => { e.stopPropagation(); onToggleMcp() }}
162-
title={mcpStatus === "started" ? `MCP running on port ${mcpPort}` : "Start MCP server"}
163-
>
164-
<McpDot $active={mcpStatus === "started"} />
165-
{mcpStatus === "started" ? `MCP :${mcpPort}` : "MCP"}
166-
</McpButton>
188+
<McpGroup>
189+
<McpButton
190+
$active={mcpStatus === "started"}
191+
onClick={(e) => { e.stopPropagation(); onToggleMcp() }}
192+
title={mcpStatus === "started" ? `MCP running on port ${mcpPort}` : "Start MCP server"}
193+
>
194+
<McpDot $active={mcpStatus === "started"} />
195+
{mcpStatus === "started" ? `MCP :${mcpPort}` : "MCP"}
196+
{mcpStatus === "started" && (
197+
mcpRedactionEnabled
198+
? <RedactionBadge title="Sensitive data is redacted"><ShieldIcon size={10} /></RedactionBadge>
199+
: <RedactionBadge $warning title="Redaction disabled — sensitive data exposed"><ShieldIcon size={10} /></RedactionBadge>
200+
)}
201+
</McpButton>
202+
{mcpStatus === "started" && (
203+
<McpSettingsButton
204+
onClick={(e) => { e.stopPropagation(); onOpenMcpSettings() }}
205+
title="MCP redaction settings"
206+
>
207+
<SettingsIcon size={14} />
208+
</McpSettingsButton>
209+
)}
210+
</McpGroup>
167211
<ExpandContainer onClick={() => setIsOpen(!isOpen)}>
168212
<ExpandIcon size={18} />
169213
</ExpandContainer>

apps/reactotron-app/src/renderer/components/Footer/index.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import StandaloneContext from "../../contexts/Standalone"
55
import Footer from "./Stateless"
66

77
export default function ConnectedFooter() {
8-
const { serverStatus, connections, selectedConnection, selectConnection, mcpStatus, mcpPort, toggleMcp } =
9-
useContext(StandaloneContext)
8+
const {
9+
serverStatus, connections, selectedConnection, selectConnection,
10+
mcpStatus, mcpPort, toggleMcp, mcpRedactionEnabled, openMcpSettings,
11+
} = useContext(StandaloneContext)
1012
const [isOpen, setIsOpen] = useState(false)
1113

1214
return (
@@ -20,6 +22,8 @@ export default function ConnectedFooter() {
2022
mcpStatus={mcpStatus}
2123
mcpPort={mcpPort}
2224
onToggleMcp={toggleMcp}
25+
mcpRedactionEnabled={mcpRedactionEnabled}
26+
onOpenMcpSettings={openMcpSettings}
2327
/>
2428
)
2529
}

0 commit comments

Comments
 (0)