Skip to content

Commit 6304b30

Browse files
feat: add MCP documentation page and modernize server (#4152)
## Summary - Add `/mcp` page with comprehensive MCP documentation (What is MCP, Configuration, Available Tools, Use Cases, Resources) - Remove deprecated SSE transport, keeping only modern Streamable HTTP - Fix FastMCP stateless_http deprecation warning using environment variable approach - Add ESLint configuration for React frontend - Fix ESLint issues in FilterBar, Layout, SpecTabs components - Extract hooks to useLayoutContext.ts to fix fast-refresh warnings - Add serena think command to planning templates - Reorder footer links symmetrically ## Test plan - [x] All 872 tests pass - [x] MCP Inspector connects successfully to `/mcp` endpoint - [x] ESLint passes with no errors - [x] Frontend builds without warnings - [x] MCP page renders correctly with all sections 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 94ff469 commit 6304b30

24 files changed

+1521
-102
lines changed

.claude/commands/bug.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ Execute every command to validate the bug is fixed with zero regressions.
8383
- `uv run pytest tests/unit` - Run unit tests
8484
- `uv run pytest tests/integration` - Run integration tests (if applicable)
8585

86+
## Final Check
87+
- Use `mcp__plugin_serena_serena__think_about_whether_you_are_done` to verify all tasks are complete.
88+
8689
## Notes
8790
<optionally list any additional notes or context that are relevant to the bug that will be helpful to the developer>
8891
```

.claude/commands/chore.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ Execute every command to validate the chore is complete with zero regressions.
6767
- `uv run pytest tests/unit` - Run unit tests
6868
- `uv run pytest tests/integration` - Run integration tests (if applicable)
6969

70+
## Final Check
71+
- Use `mcp__plugin_serena_serena__think_about_whether_you_are_done` to verify all tasks are complete.
72+
7073
## Notes
7174
<optionally list any additional notes or context that are relevant to the chore that will be helpful to the developer>
7275
```

.claude/commands/feature.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ Execute every command to validate the feature works correctly with zero regressi
104104
- `uv run pytest tests/unit` - Run unit tests
105105
- `uv run pytest tests/integration` - Run integration tests (if applicable)
106106

107+
## Final Check
108+
- Use `mcp__plugin_serena_serena__think_about_whether_you_are_done` to verify all tasks are complete.
109+
107110
## Notes
108111
<optionally list any additional notes, future considerations, or context that are relevant to the feature that will be helpful to the developer>
109112
```

.claude/commands/implement.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,7 @@ $ARGUMENTS
99

1010
## Report
1111
- Summarize the work you've just done in a concise bullet point list.
12-
- Report the files and total lines changed with `git diff --stat`
12+
- Report the files and total lines changed with `git diff --stat`
13+
14+
## Final Check
15+
- Use `mcp__plugin_serena_serena__think_about_whether_you_are_done` to verify all tasks are complete.

api/main.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,8 @@
4343
logger = logging.getLogger(__name__)
4444

4545

46-
# Create MCP HTTP apps (needed for lifespan integration)
47-
# Streamable HTTP (modern, recommended) - used by newer clients
46+
# Create MCP HTTP app (needed for lifespan integration)
4847
mcp_http_app = mcp_server.http_app(path="/")
49-
# SSE transport (legacy, wider compatibility) - used by mcp-remote, older clients
50-
mcp_sse_app = mcp_server.http_app(path="/", transport="sse")
5148

5249

5350
@asynccontextmanager
@@ -63,11 +60,10 @@ async def lifespan(app: FastAPI):
6360
except Exception as e:
6461
logger.error(f"Failed to initialize database: {e}")
6562

66-
# Initialize MCP server lifespans (required for session management)
63+
# Initialize MCP server lifespan
6764
async with mcp_http_app.lifespan(app):
68-
async with mcp_sse_app.lifespan(app):
69-
logger.info("MCP server initialized (HTTP + SSE)")
70-
yield
65+
logger.info("MCP server initialized")
66+
yield
7167

7268
# Cleanup database connection
7369
logger.info("Shutting down pyplots API...")
@@ -136,10 +132,8 @@ async def add_cache_headers(request: Request, call_next):
136132
return response
137133

138134

139-
# Mount MCP servers for AI assistant integration
140-
# Note: Apps are created earlier with lifespan integration
141-
app.mount("/mcp", mcp_http_app) # Streamable HTTP at /mcp
142-
app.mount("/sse", mcp_sse_app) # SSE transport at /sse
135+
# Mount MCP server for AI assistant integration
136+
app.mount("/mcp", mcp_http_app)
143137

144138
# Register routers
145139
app.include_router(health_router)

api/mcp/server.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ async def get_mcp_db_session() -> AsyncSession:
6464
return _mcp_session_factory()
6565

6666

67+
# Enable stateless HTTP mode via environment variable (recommended approach)
68+
# This allows horizontal scaling without session affinity
69+
os.environ.setdefault("FASTMCP_STATELESS_HTTP", "true")
70+
6771
# Initialize FastMCP server
6872
mcp_server = FastMCP("pyplots")
6973

app/eslint.config.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import js from '@eslint/js';
2+
import tseslint from '@typescript-eslint/eslint-plugin';
3+
import tsparser from '@typescript-eslint/parser';
4+
import reactHooks from 'eslint-plugin-react-hooks';
5+
import reactRefresh from 'eslint-plugin-react-refresh';
6+
7+
export default [
8+
js.configs.recommended,
9+
{
10+
files: ['src/**/*.{ts,tsx}'],
11+
languageOptions: {
12+
parser: tsparser,
13+
parserOptions: {
14+
ecmaVersion: 'latest',
15+
sourceType: 'module',
16+
ecmaFeatures: {
17+
jsx: true,
18+
},
19+
},
20+
globals: {
21+
// Browser globals
22+
window: 'readonly',
23+
document: 'readonly',
24+
navigator: 'readonly',
25+
console: 'readonly',
26+
setTimeout: 'readonly',
27+
clearTimeout: 'readonly',
28+
setInterval: 'readonly',
29+
clearInterval: 'readonly',
30+
fetch: 'readonly',
31+
URL: 'readonly',
32+
URLSearchParams: 'readonly',
33+
localStorage: 'readonly',
34+
history: 'readonly',
35+
location: 'readonly',
36+
requestAnimationFrame: 'readonly',
37+
cancelAnimationFrame: 'readonly',
38+
// DOM types
39+
HTMLElement: 'readonly',
40+
HTMLDivElement: 'readonly',
41+
HTMLInputElement: 'readonly',
42+
HTMLButtonElement: 'readonly',
43+
HTMLAnchorElement: 'readonly',
44+
HTMLImageElement: 'readonly',
45+
HTMLIFrameElement: 'readonly',
46+
Element: 'readonly',
47+
Node: 'readonly',
48+
NodeList: 'readonly',
49+
// Events
50+
MouseEvent: 'readonly',
51+
KeyboardEvent: 'readonly',
52+
TouchEvent: 'readonly',
53+
ClipboardEvent: 'readonly',
54+
Event: 'readonly',
55+
MessageEvent: 'readonly',
56+
// APIs
57+
AbortController: 'readonly',
58+
RequestInit: 'readonly',
59+
Response: 'readonly',
60+
ResizeObserver: 'readonly',
61+
IntersectionObserver: 'readonly',
62+
MutationObserver: 'readonly',
63+
Blob: 'readonly',
64+
File: 'readonly',
65+
FileReader: 'readonly',
66+
// React (for JSX runtime)
67+
React: 'readonly',
68+
},
69+
},
70+
plugins: {
71+
'@typescript-eslint': tseslint,
72+
'react-hooks': reactHooks,
73+
'react-refresh': reactRefresh,
74+
},
75+
rules: {
76+
...tseslint.configs.recommended.rules,
77+
...reactHooks.configs.recommended.rules,
78+
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
79+
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
80+
'no-unused-vars': 'off',
81+
// Allow ref updates during render (common pattern for keeping refs in sync)
82+
'react-hooks/refs': 'off',
83+
},
84+
},
85+
{
86+
ignores: ['dist/**', 'node_modules/**', '*.config.js'],
87+
},
88+
];

app/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"dev": "vite",
99
"build": "tsc && vite build",
1010
"preview": "vite preview",
11-
"lint": "eslint src --ext ts,tsx",
11+
"lint": "eslint src",
1212
"type-check": "tsc --noEmit",
1313
"test": "vitest run",
1414
"test:watch": "vitest"
@@ -30,11 +30,17 @@
3030
"react-syntax-highlighter": "^16.1.0"
3131
},
3232
"devDependencies": {
33+
"@eslint/js": "^9.39.2",
3334
"@types/react": "^19.2.8",
3435
"@types/react-dom": "^19.1.7",
3536
"@types/react-syntax-highlighter": "^15.5.13",
37+
"@typescript-eslint/eslint-plugin": "^8.54.0",
38+
"@typescript-eslint/parser": "^8.54.0",
3639
"@vitejs/plugin-react-swc": "^4.0.0",
3740
"@vitest/coverage-v8": "^4.0.17",
41+
"eslint": "^9.39.2",
42+
"eslint-plugin-react-hooks": "^7.0.1",
43+
"eslint-plugin-react-refresh": "^0.4.26",
3844
"typescript": "^5.9.2",
3945
"vite": "^7.3.1",
4046
"vitest": "^4.0.17"

app/src/components/FilterBar.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export function FilterBar({
116116
setIsSearchManuallyExpanded(true);
117117
setDropdownAnchor(searchContainerRef.current);
118118
setTimeout(() => inputRef.current?.focus(), 0);
119-
}, []);
119+
}, [inputRef]);
120120

121121
// Collapse when empty and loses focus (only if there are filters)
122122
const handleSearchBlur = useCallback(() => {
@@ -133,15 +133,17 @@ export function FilterBar({
133133
setDropdownAnchor(null);
134134
setSelectedCategory(null);
135135
setSearchQuery('');
136+
setHighlightedIndex(-1);
136137
setIsSearchManuallyExpanded(false);
137138
}, []);
138139

139140
// Select category from dropdown
140141
const handleCategorySelect = useCallback((category: FilterCategory) => {
141142
setSelectedCategory(category);
142143
setSearchQuery('');
144+
setHighlightedIndex(-1);
143145
setTimeout(() => inputRef.current?.focus(), 50);
144-
}, []);
146+
}, [inputRef]);
145147

146148
// Select value (add new filter group)
147149
const handleValueSelect = useCallback(
@@ -153,14 +155,15 @@ export function FilterBar({
153155
}
154156
setSelectedCategory(null);
155157
setSearchQuery('');
158+
setHighlightedIndex(-1);
156159
// Keep expanded and focused for next filter
157160
setIsSearchManuallyExpanded(true);
158161
setTimeout(() => {
159162
setDropdownAnchor(searchContainerRef.current);
160163
inputRef.current?.focus();
161164
}, 50);
162165
},
163-
[onAddFilter, onTrackEvent, searchQuery]
166+
[onAddFilter, onTrackEvent, searchQuery, inputRef]
164167
);
165168

166169
// Chip click - open chip menu
@@ -265,11 +268,6 @@ export function FilterBar({
265268

266269
const dropdownItems = getDropdownItems();
267270

268-
// Reset highlight when dropdown content changes
269-
useEffect(() => {
270-
setHighlightedIndex(-1);
271-
}, [searchQuery, selectedCategory, dropdownAnchor]);
272-
273271
// Handle keyboard navigation
274272
const handleKeyDown = useCallback(
275273
(event: React.KeyboardEvent) => {
@@ -295,7 +293,7 @@ export function FilterBar({
295293
inputRef.current?.blur();
296294
}
297295
},
298-
[dropdownItems, highlightedIndex, handleCategorySelect, handleValueSelect, handleDropdownClose]
296+
[dropdownItems, highlightedIndex, handleCategorySelect, handleValueSelect, handleDropdownClose, inputRef]
299297
);
300298

301299
// Get active group for chip menu
@@ -471,6 +469,7 @@ export function FilterBar({
471469
value={searchQuery}
472470
onChange={(e) => {
473471
setSearchQuery(e.target.value);
472+
setHighlightedIndex(-1);
474473
if (!dropdownAnchor) {
475474
setDropdownAnchor(searchContainerRef.current);
476475
}
@@ -480,6 +479,7 @@ export function FilterBar({
480479
setIsSearchManuallyExpanded(true);
481480
}
482481
setDropdownAnchor(searchContainerRef.current);
482+
setHighlightedIndex(-1);
483483
}}
484484
onBlur={handleSearchBlur}
485485
onKeyDown={handleKeyDown}

app/src/components/Footer.tsx

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,45 +24,58 @@ export function Footer({ onTrackEvent, selectedSpec, selectedLibrary }: FooterPr
2424
}}
2525
>
2626
<Link
27-
href="https://www.linkedin.com/in/markus-neusinger/"
27+
href={GITHUB_URL}
2828
target="_blank"
2929
rel="noopener noreferrer"
30-
onClick={() => onTrackEvent?.('external_link', { destination: 'linkedin', spec: selectedSpec, library: selectedLibrary })}
30+
onClick={() => onTrackEvent?.('external_link', { destination: 'github', spec: selectedSpec, library: selectedLibrary })}
3131
sx={{
3232
color: '#9ca3af',
3333
textDecoration: 'none',
3434
'&:hover': { color: '#6b7280' },
3535
}}
3636
>
37-
markus neusinger
37+
github
3838
</Link>
3939
<span>·</span>
4040
<Link
41-
href={GITHUB_URL}
41+
href="https://plausible.io/pyplots.ai"
4242
target="_blank"
4343
rel="noopener noreferrer"
44-
onClick={() => onTrackEvent?.('external_link', { destination: 'github', spec: selectedSpec, library: selectedLibrary })}
44+
onClick={() => onTrackEvent?.('external_link', { destination: 'stats', spec: selectedSpec, library: selectedLibrary })}
4545
sx={{
4646
color: '#9ca3af',
4747
textDecoration: 'none',
4848
'&:hover': { color: '#6b7280' },
4949
}}
5050
>
51-
github
51+
stats
5252
</Link>
5353
<span>·</span>
5454
<Link
55-
href="https://plausible.io/pyplots.ai"
55+
href="https://www.linkedin.com/in/markus-neusinger/"
5656
target="_blank"
5757
rel="noopener noreferrer"
58-
onClick={() => onTrackEvent?.('external_link', { destination: 'stats', spec: selectedSpec, library: selectedLibrary })}
58+
onClick={() => onTrackEvent?.('external_link', { destination: 'linkedin', spec: selectedSpec, library: selectedLibrary })}
5959
sx={{
6060
color: '#9ca3af',
6161
textDecoration: 'none',
6262
'&:hover': { color: '#6b7280' },
6363
}}
6464
>
65-
stats
65+
markus neusinger
66+
</Link>
67+
<span>·</span>
68+
<Link
69+
component={RouterLink}
70+
to="/mcp"
71+
onClick={() => onTrackEvent?.('internal_link', { destination: 'mcp', spec: selectedSpec, library: selectedLibrary })}
72+
sx={{
73+
color: '#9ca3af',
74+
textDecoration: 'none',
75+
'&:hover': { color: '#6b7280' },
76+
}}
77+
>
78+
mcp
6679
</Link>
6780
<span>·</span>
6881
<Link

0 commit comments

Comments
 (0)