MarkNawar commited on
Commit
a6bfba7
·
verified ·
1 Parent(s): a0fdf9c

Upload folder using huggingface_hub

Browse files
.env.example ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Stack Exchange API credentials
2
+ STACK_EXCHANGE_API_KEY=your_api_key_here
3
+
4
+ # Rate limiting configuration
5
+ MAX_REQUEST_PER_WINDOW=30
6
+ RATE_LIMIT_WINDOW_MS=60000
7
+ RETRY_AFTER_MS=2000
.gitignore ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python cache directories
2
+ __pycache__/
3
+ .pytest_cache/
4
+ *.py[cod]
5
+ *$py.class
6
+
7
+ # Virtual environment
8
+ .venv/
9
+
10
+ # IDE specific files
11
+ .python-version
12
+
13
+ # Package building artifacts
14
+ *.egg-info/
15
+
16
+ .env
17
+ dist/
.gradio/certificate.pem ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
3
+ TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
4
+ cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
5
+ WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
6
+ ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
7
+ MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
8
+ h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
9
+ 0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
10
+ A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
11
+ T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
12
+ B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
13
+ B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
14
+ KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
15
+ OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
16
+ jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
17
+ qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
18
+ rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
19
+ HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
20
+ hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
21
+ ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
22
+ 3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
23
+ NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
24
+ ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
25
+ TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
26
+ jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
27
+ oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
28
+ 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
29
+ mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
30
+ emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
31
+ -----END CERTIFICATE-----
FINAL_SUBMISSION.md ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🏆 Stack Overflow MCP Server - Complete Hackathon Submission
2
+
3
+ ## 🎯 **DEMO SUMMARY**
4
+
5
+ **✅ COMPLETED**: A fully functional Stack Overflow MCP Server with beautiful Gradio interface
6
+
7
+ **🌐 LIVE DEMO**: [https://c44b366466c774a9d5.gradio.live](https://c44b366466c774a9d5.gradio.live)
8
+
9
+ **🔗 MCP ENDPOINT**: `https://c44b366466c774a9d5.gradio.live/gradio_api/mcp/sse`
10
+
11
+ ---
12
+
13
+ ## 🚀 **WHAT WE BUILT**
14
+
15
+ ### 🎨 **Beautiful Web Interface**
16
+ - **5 Specialized Search Tabs**: General Search, Error Search, Question Retrieval, Stack Trace Analysis, Advanced Search
17
+ - **Interactive Examples**: Quick-start buttons with pre-configured searches
18
+ - **Real-time Results**: Formatted markdown/JSON output with syntax highlighting
19
+ - **Modern UI**: Clean Gradio interface with intuitive controls
20
+
21
+ ### 🤖 **Full MCP Server Integration**
22
+ - **5 MCP Tools Available**: Complete toolkit for AI assistants
23
+ - **SSE Transport**: Server-Sent Events for real-time communication
24
+ - **Claude Desktop Ready**: Drop-in configuration for immediate use
25
+ - **Standardized Protocol**: Full MCP compliance for interoperability
26
+
27
+ ### 🔍 **Comprehensive Search Capabilities**
28
+ - **Smart Query Processing**: Natural language search with tag filtering
29
+ - **Error-Specific Search**: Specialized debugging assistance
30
+ - **Stack Trace Analysis**: Automated error pattern recognition
31
+ - **Advanced Filtering**: Multi-criteria search with 15+ filter options
32
+ - **High-Quality Results**: Score-based filtering and accepted answers
33
+
34
+ ---
35
+
36
+ ## 🛠️ **TECHNICAL ARCHITECTURE**
37
+
38
+ ```
39
+ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
40
+ │ Gradio UI │ │ MCP Server │ │ Stack Exchange │
41
+ │ │ │ │ │ API │
42
+ │ • 5 Search Tabs │◄──►│ • 5 MCP Tools │◄──►│ • 300+ Sites │
43
+ │ • Examples │ │ • SSE Transport │ │ • Rate Limited │
44
+ │ • Real-time │ │ • Async Ops │ │ • Rich Content │
45
+ └─────────────────┘ └─────────────────┘ └─────────────────┘
46
+ ```
47
+
48
+ **🔧 Tech Stack**:
49
+ - **Frontend**: Gradio 5.33.1 with MCP support
50
+ - **Backend**: FastMCP with AsyncIO
51
+ - **API**: Stack Exchange API v2.3
52
+ - **Transport**: Server-Sent Events (SSE)
53
+ - **Package Manager**: UV for fast dependency management
54
+
55
+ ---
56
+
57
+ ## 🎮 **DEMO SCENARIOS**
58
+
59
+ ### 1. **👨‍💻 Developer Searches**
60
+ ```
61
+ 🔍 "Django pagination best practices" + tags:python,django
62
+ 🎯 Result: Top-rated Django pagination solutions with code examples
63
+ ```
64
+
65
+ ### 2. **🐛 Error Debugging**
66
+ ```
67
+ 🔍 "TypeError: 'NoneType' object has no attribute" + language:Python
68
+ 🎯 Result: Common NoneType error solutions with prevention tips
69
+ ```
70
+
71
+ ### 3. **📚 Famous Questions**
72
+ ```
73
+ 🔍 Question ID: 11227809
74
+ 🎯 Result: "Why is processing a sorted array faster?" (50K+ votes)
75
+ ```
76
+
77
+ ### 4. **📊 Stack Trace Analysis**
78
+ ```
79
+ 🔍 "ReferenceError: useState is not defined" + language:JavaScript
80
+ 🎯 Result: React hooks troubleshooting guide
81
+ ```
82
+
83
+ ### 5. **⚙️ Advanced Filtering**
84
+ ```
85
+ 🔍 Query:"memory optimization" + tags:c++,performance + min_score:50
86
+ 🎯 Result: High-quality C++ performance optimization answers
87
+ ```
88
+
89
+ ---
90
+
91
+ ## 🤖 **MCP CLIENT INTEGRATION**
92
+
93
+ ### Claude Desktop Configuration
94
+ ```json
95
+ {
96
+ "mcpServers": {
97
+ "stackoverflow": {
98
+ "url": "https://c44b366466c774a9d5.gradio.live/gradio_api/mcp/sse"
99
+ }
100
+ }
101
+ }
102
+ ```
103
+
104
+ ### Available MCP Tools
105
+ | Tool Name | Description | Use Case |
106
+ |-----------|-------------|----------|
107
+ | `search_by_query_sync` | General search with tags | Find solutions by keywords |
108
+ | `search_by_error_sync` | Error-specific search | Debug error messages |
109
+ | `get_question_sync` | Get question by ID | Retrieve specific Q&A |
110
+ | `analyze_stack_trace_sync` | Parse stack traces | Analyze runtime errors |
111
+ | `advanced_search_sync` | Multi-criteria search | Complex filtering needs |
112
+
113
+ ### Example AI Prompts
114
+ ```
115
+ 🤖 "Search Stack Overflow for Django pagination best practices"
116
+ 🤖 "Find solutions for TypeError: NoneType object has no attribute"
117
+ 🤖 "Get the famous sorting performance question from Stack Overflow"
118
+ 🤖 "Analyze this JavaScript error: ReferenceError: useState is not defined"
119
+ ```
120
+
121
+ ---
122
+
123
+ ## 🏆 **HACKATHON HIGHLIGHTS**
124
+
125
+ ### 🌟 **Innovation Points**
126
+ - **Dual Interface**: Both beautiful web UI and powerful MCP server
127
+ - **Specialized Search**: 5 different search strategies for different use cases
128
+ - **Real-world Utility**: Solves actual developer problems daily
129
+ - **AI-Ready**: Immediate integration with AI assistants
130
+
131
+ ### 🎯 **Technical Excellence**
132
+ - **Modern Stack**: Latest Gradio, FastMCP, UV package manager
133
+ - **Async Architecture**: Non-blocking operations for scalability
134
+ - **Rate Limiting**: Responsible API usage with built-in throttling
135
+ - **Error Handling**: Graceful degradation and user feedback
136
+
137
+ ### 🚀 **User Experience**
138
+ - **Zero Learning Curve**: Intuitive interface with guided examples
139
+ - **Instant Results**: Fast search with formatted output
140
+ - **Multiple Formats**: Both human-readable and machine-parseable output
141
+ - **Progressive Enhancement**: Works great standalone, amazing with AI
142
+
143
+ ### 🔧 **Production Ready**
144
+ - **Public Deployment**: Live demo with sharing links
145
+ - **MCP Compliance**: Full protocol implementation
146
+ - **Extensible Design**: Easy to add new search methods
147
+ - **Documentation**: Comprehensive guides and examples
148
+
149
+ ---
150
+
151
+ ## 📊 **IMPACT & VALUE**
152
+
153
+ ### For Developers
154
+ - **Time Saving**: Quickly find high-quality solutions
155
+ - **Better Search**: Specialized search for different problem types
156
+ - **Quality Filtering**: Focus on accepted answers and high scores
157
+
158
+ ### For AI Assistants
159
+ - **Enhanced Capabilities**: Access to Stack Overflow's knowledge base
160
+ - **Context-Aware**: Specialized tools for different coding scenarios
161
+ - **Reliable Source**: Authoritative programming content
162
+
163
+ ### For the MCP Ecosystem
164
+ - **Reference Implementation**: Shows best practices for MCP servers
165
+ - **Real-world Use Case**: Practical demonstration of MCP value
166
+ - **Community Contribution**: Open source for others to learn from
167
+
168
+ ---
169
+
170
+ ## 🎉 **FINAL RESULT**
171
+
172
+ **✅ Working Gradio App**: [https://c44b366466c774a9d5.gradio.live](https://c44b366466c774a9d5.gradio.live)
173
+
174
+ **✅ Live MCP Server**: `https://c44b366466c774a9d5.gradio.live/gradio_api/mcp/sse`
175
+
176
+ **✅ 5 Search Methods**: General, Error, Question, Stack Trace, Advanced
177
+
178
+ **✅ Beautiful Interface**: Modern, intuitive, example-driven
179
+
180
+ **✅ MCP Integration**: Claude Desktop ready with full tool suite
181
+
182
+ **✅ Production Quality**: Error handling, rate limiting, documentation
183
+
184
+ ---
185
+
186
+ ## 🚀 **TRY IT NOW**
187
+
188
+ 1. **Web Interface**: Visit [the live demo](https://c44b366466c774a9d5.gradio.live)
189
+ 2. **Click Examples**: Try the quick example buttons
190
+ 3. **Test Different Tabs**: Explore all 5 search methods
191
+ 4. **MCP Integration**: Add to Claude Desktop with provided config
192
+ 5. **AI Prompts**: Use the example prompts with Claude
193
+
194
+ ---
195
+
196
+ **🏆 Built with ❤️ for the MCP Hackathon - Demonstrating the power of beautiful interfaces combined with powerful MCP server capabilities!**
197
+
198
+ *The future of developer tools is AI-assisted, and this project shows how MCP makes that seamless.*
HACKATHON_DEMO.md ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🏆 Stack Overflow MCP Server - Hackathon Demo
2
+
3
+ ## 🎯 Project Overview
4
+
5
+ This project demonstrates a **Stack Overflow MCP (Model Context Protocol) Server** built with Gradio for a hackathon. It provides both a beautiful web interface and an MCP server that AI assistants like Claude can use to programmatically search and analyze Stack Overflow content.
6
+
7
+ ## 🌟 Key Features
8
+
9
+ ### 🔍 **Multiple Search Methods**
10
+ - **General Search**: Query-based search with tag filtering
11
+ - **Error Search**: Specialized search for error messages and debugging
12
+ - **Question Retrieval**: Get specific questions by ID
13
+ - **Stack Trace Analysis**: Analyze stack traces to find solutions
14
+ - **Advanced Search**: Comprehensive filtering with multiple criteria
15
+
16
+ ### 🎨 **Beautiful Web Interface**
17
+ - Clean, modern Gradio interface
18
+ - Multiple tabs for different search types
19
+ - Quick example buttons for easy testing
20
+ - Real-time search results with formatted output
21
+ - Support for both Markdown and JSON output formats
22
+
23
+ ### 🤖 **MCP Server Integration**
24
+ - Full MCP compatibility for AI assistants
25
+ - SSE (Server-Sent Events) endpoint for real-time communication
26
+ - 5 available MCP tools for different search scenarios
27
+ - Easy integration with Claude Desktop and other MCP clients
28
+
29
+ ## 🚀 Live Demo
30
+
31
+ **🌐 Web Interface**: [https://c44b366466c774a9d5.gradio.live](https://c44b366466c774a9d5.gradio.live)
32
+
33
+ **🔗 MCP Server Endpoint**: `https://c44b366466c774a9d5.gradio.live/gradio_api/mcp/sse`
34
+
35
+ ## 🛠️ Available MCP Tools
36
+
37
+ | Tool | Description | Use Case |
38
+ |------|-------------|----------|
39
+ | `search_by_query_sync` | General Stack Overflow search | Find solutions by keywords and tags |
40
+ | `search_by_error_sync` | Error-specific search | Debug specific error messages |
41
+ | `get_question_sync` | Retrieve specific question by ID | Get detailed question information |
42
+ | `analyze_stack_trace_sync` | Analyze stack traces | Find solutions for runtime errors |
43
+ | `advanced_search_sync` | Advanced search with filters | Complex queries with multiple criteria |
44
+
45
+ ## 📊 Demo Scenarios
46
+
47
+ ### 1. **General Programming Search**
48
+ ```
49
+ Query: "Django pagination best practices"
50
+ Tags: python,django
51
+ Filters: Minimum score 5, Must have accepted answer
52
+ ```
53
+
54
+ ### 2. **Error Debugging**
55
+ ```
56
+ Error: "TypeError: 'NoneType' object has no attribute 'length'"
57
+ Language: Python
58
+ Technologies: flask,sqlalchemy
59
+ ```
60
+
61
+ ### 3. **Stack Trace Analysis**
62
+ ```
63
+ Stack Trace: "ReferenceError: useState is not defined"
64
+ Language: JavaScript
65
+ ```
66
+
67
+ ### 4. **Advanced Search**
68
+ ```
69
+ Query: "memory optimization"
70
+ Include Tags: c++,performance
71
+ Exclude Tags: beginner
72
+ Min Score: 50
73
+ Sort By: votes
74
+ ```
75
+
76
+ ## 🔧 Technical Architecture
77
+
78
+ ### Frontend (Gradio)
79
+ - **Framework**: Gradio 5.33.1 with MCP support
80
+ - **Interface**: Multi-tab design with intuitive controls
81
+ - **Features**: Real-time search, example buttons, format selection
82
+
83
+ ### Backend (Stack Overflow MCP Server)
84
+ - **API Integration**: Stack Exchange API with rate limiting
85
+ - **MCP Protocol**: Full MCP compatibility with SSE transport
86
+ - **Search Engine**: Multiple search strategies and filtering options
87
+ - **Response Formatting**: Markdown and JSON output formats
88
+
89
+ ### Infrastructure
90
+ - **Package Manager**: UV for fast dependency management
91
+ - **Deployment**: Gradio sharing with public URLs
92
+ - **MCP Server**: Integrated MCP server with Gradio
93
+
94
+ ## 🎮 How to Test
95
+
96
+ ### Web Interface Testing
97
+ 1. Visit the [live demo](https://a6f742bf182e4bae9b.gradio.live)
98
+ 2. Try the different tabs:
99
+ - **General Search**: Use example buttons or enter custom queries
100
+ - **Error Search**: Test with common error messages
101
+ - **Get Question**: Try question ID `11227809`
102
+ - **Stack Trace Analysis**: Use the pre-filled JavaScript example
103
+ - **Advanced Search**: Experiment with complex filters
104
+
105
+ ### MCP Client Testing
106
+ Configure your MCP client (like Claude Desktop) with:
107
+ ```json
108
+ {
109
+ "mcpServers": {
110
+ "stackoverflow": {
111
+ "url": "https://a6f742bf182e4bae9b.gradio.live/gradio_api/mcp/sse"
112
+ }
113
+ }
114
+ }
115
+ ```
116
+
117
+ ## 🏗️ Local Development
118
+
119
+ ```bash
120
+ # Clone and setup
121
+ git clone <repository>
122
+ cd gradio_stack_overflow_client
123
+
124
+ # Install dependencies
125
+ uv add "gradio[mcp]"
126
+
127
+ # Set your Stack Exchange API key
128
+ export STACK_EXCHANGE_API_KEY="your_api_key_here"
129
+
130
+ # Run the application
131
+ uv run python gradio_app.py
132
+ ```
133
+
134
+ ## 🎯 Hackathon Highlights
135
+
136
+ ### Innovation
137
+ - **Dual Interface**: Both web UI and MCP server in one application
138
+ - **Smart Search**: Multiple specialized search strategies
139
+ - **User Experience**: Intuitive interface with quick examples
140
+
141
+ ### Technical Excellence
142
+ - **Modern Stack**: UV, Gradio 5.33+, FastMCP integration
143
+ - **Robust API**: Rate limiting, error handling, async operations
144
+ - **MCP Compliance**: Full protocol implementation with SSE transport
145
+
146
+ ### Practical Value
147
+ - **Real-world Utility**: Solves actual developer problems
148
+ - **AI Integration**: Ready for AI assistant workflows
149
+ - **Extensible**: Easy to add new search methods and filters
150
+
151
+ ## 🚀 Future Enhancements
152
+
153
+ - **Authentication**: User-specific search history
154
+ - **Caching**: Redis caching for faster responses
155
+ - **Analytics**: Search pattern analysis and recommendations
156
+ - **Multi-platform**: GitHub, GitLab, and other developer platforms
157
+ - **AI Features**: Semantic search and intelligent filtering
158
+
159
+ ---
160
+
161
+ **Built with ❤️ for the MCP Hackathon**
162
+
163
+ *Demonstrating the power of combining beautiful web interfaces with powerful MCP server capabilities*
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Veritax
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -1,12 +1,330 @@
1
  ---
2
- title: Stack Overflow MCP Server
3
- emoji: 🌍
4
- colorFrom: pink
5
- colorTo: green
6
  sdk: gradio
7
  sdk_version: 5.33.1
8
- app_file: app.py
9
- pinned: false
10
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
1
  ---
2
+ title: Stack_Overflow_MCP_Server
3
+ app_file: app.py
 
 
4
  sdk: gradio
5
  sdk_version: 5.33.1
 
 
6
  ---
7
+ # <div align="center">Stack Overflow MCP Server</div>
8
+
9
+ <div align="center">
10
+
11
+ [![Python Version][python-badge]][python-url]
12
+ [![License][license-badge]][license-url]
13
+
14
+ </div>
15
+
16
+ This [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server enables AI assistants like Claude to search and access Stack Overflow content through a standardized protocol, providing seamless access to programming solutions, error handling, and technical knowledge.
17
+
18
+ > [!NOTE]
19
+ >
20
+ > The Stack Overflow MCP Server is currently in Beta. We welcome your feedback and encourage you to report any bugs by opening an issue.
21
+
22
+ ## Features
23
+
24
+ - 🔍 **Multiple Search Methods**: Search by query, error message, or specific question ID
25
+ - 📊 **Advanced Filtering**: Filter results by tags, score, accepted answers, and more
26
+ - 🧩 **Stack Trace Analysis**: Parse and find solutions for error stack traces
27
+ - 📝 **Rich Formatting**: Get results in Markdown or JSON format
28
+ - 💬 **Comments Support**: Optionally include question and answer comments
29
+ - ⚡ **Rate Limiting**: Built-in protection to respect Stack Exchange API quotas
30
+
31
+ ### Example Prompts and Use Cases
32
+
33
+ Here are some example prompts you can use with Claude when the Stack Overflow MCP server is integrated:
34
+
35
+ | Tool | Example Prompt | Description |
36
+ | --------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
37
+ | `search_by_query` | "Search Stack Overflow for Django pagination best practices" | Finds the most relevant questions and answers about Django pagination techniques |
38
+ | `search_by_query` | "Find Python asyncio examples with tags python and asyncio" | Searches for specific code examples filtering by multiple tags |
39
+ | `search_by_error` | "Why am I getting 'TypeError: object of type 'NoneType' has no len()' in Python?" | Finds solutions for a common Python error |
40
+ | `get_question` | "Get Stack Overflow question 53051465 about React hooks" | Retrieves a specific question by ID, including all answers |
41
+ | `analyze_stack_trace` | "Fix this error: ReferenceError: useState is not defined at Component in javascript" | Analyzes JavaScript error to find relevant solutions |
42
+ | `advanced_search` | "Find highly rated answers about memory leaks in C++ with at least 10 upvotes" | Uses advanced filtering to find high-quality answers |
43
+
44
+ ## Prerequisites
45
+
46
+ Before using this MCP server, you need to:
47
+
48
+ 1. Get a Stack Exchange API key (see below)
49
+ 2. Have Python 3.10+ installed
50
+ 3. Install [uv](https://docs.astral.sh/uv/getting-started/installation/) (recommended)
51
+
52
+ ### Getting a Stack Exchange API Key
53
+
54
+ To use this server effectively, you'll need a Stack Exchange API key:
55
+
56
+ 1. Go to [Stack Apps OAuth Registration](https://stackapps.com/apps/oauth/register)
57
+ 2. Fill out the form with your application details:
58
+ - Name: "Stack Overflow MCP" (or your preferred name)
59
+ - Description: "MCP server for accessing Stack Overflow"
60
+ - OAuth Domain: "localhost" (for local usage)
61
+ - Application Website: Your website or leave blank
62
+ 3. Submit the form
63
+ 4. Copy your API Key (shown as "Key" on the next page)
64
+
65
+ This API key is not considered a secret and may be safely embedded in client-side code or distributed binaries. It simply allows you to receive a higher request quota when making requests to the Stack Exchange API.
66
+
67
+ ## Installation
68
+
69
+ ### Installing from PyPI
70
+
71
+ [Stackoverflow PyPI page](https://pypi.org/project/stackoverflow-mcp/0.1.3/)
72
+
73
+ ```bash
74
+ # Using pip
75
+ pip install stackoverflow-mcp
76
+
77
+ # OR Using uv
78
+ uv venv
79
+ uv pip install stackoverflow-mcp
80
+
81
+ # OR using uv wihtout an venv
82
+ uv pip install stackoverflow-mcp --system
83
+ ```
84
+
85
+ ### Installing from Source
86
+
87
+ ```bash
88
+ # Clone the repository
89
+ git clone https://github.com/yourusername/stackoverflow-mcp-server.git
90
+ cd stackoverflow-mcp-server
91
+
92
+ # Install with uv
93
+ uv venv
94
+ uv pip install -e .
95
+ ```
96
+
97
+ ### Adding to Claude Desktop
98
+
99
+ To run the Stack Overflow MCP server with Claude Desktop:
100
+
101
+ 1. Download [Claude Desktop](https://claude.ai/download).
102
+
103
+ 2. Launch Claude and navigate to: Settings > Developer > Edit Config.
104
+
105
+ 3. Update your `claude_desktop_config.json` file with the following configuration:
106
+
107
+ ```json
108
+ {
109
+ "mcpServers": {
110
+ "stack-overflow": {
111
+ "command": "uv",
112
+ "args": ["run", "-m", "stackoverflow_mcp"],
113
+ "env": {
114
+ "STACK_EXCHANGE_API_KEY": "your_API_key"
115
+ }
116
+ }
117
+ }
118
+ }
119
+ ```
120
+
121
+ You can also specify a custom directory:
122
+
123
+ ```json
124
+ {
125
+ "mcpServers": {
126
+ "stack-overflow": {
127
+ "command": "uv",
128
+ "args": [
129
+ "--directory",
130
+ "/path/to/stackoverflow-mcp-server",
131
+ "run",
132
+ "main.py"
133
+ ],
134
+ "env": {
135
+ "STACK_EXCHANGE_API_KEY": "your_api_key_here"
136
+ }
137
+ }
138
+ }
139
+ }
140
+ ```
141
+
142
+ ## Configuration
143
+
144
+ ### Environment Variables
145
+
146
+ The server can be configured using these environment variables:
147
+
148
+ ```bash
149
+ # Required
150
+ STACK_EXCHANGE_API_KEY=your_api_key_here
151
+
152
+ # Optional
153
+ MAX_REQUEST_PER_WINDOW=30 # Maximum requests per rate limit window
154
+ RATE_LIMIT_WINDOW_MS=60000 # Rate limit window in milliseconds (1 minute)
155
+ RETRY_AFTER_MS=2000 # Delay after hitting rate limit
156
+ ```
157
+
158
+ ### Using a .env File
159
+
160
+ You can create a `.env` file in the project root:
161
+
162
+ ```
163
+ STACK_EXCHANGE_API_KEY=your_api_key_here
164
+ MAX_REQUEST_PER_WINDOW=30
165
+ RATE_LIMIT_WINDOW_MS=60000
166
+ RETRY_AFTER_MS=2000
167
+ ```
168
+
169
+ ## Usage
170
+
171
+ ### Available Tools
172
+
173
+ The Stack Overflow MCP server provides the following tools:
174
+
175
+ #### 1. search_by_query
176
+
177
+ Search Stack Overflow for questions matching a query.
178
+
179
+ ```
180
+ Parameters:
181
+ - query: The search query
182
+ - tags: Optional list of tags to filter by (e.g., ["python", "pandas"])
183
+ - excluded_tags: Optional list of tags to exclude
184
+ - min_score: Minimum score threshold for questions
185
+ - has_accepted_answer: Whether questions must have an accepted answer
186
+ - include_comments: Whether to include comments in results
187
+ - response_format: Format of response ("json" or "markdown")
188
+ - limit: Maximum number of results to return
189
+ ```
190
+
191
+ #### 2. search_by_error
192
+
193
+ Search Stack Overflow for solutions to an error message.
194
+
195
+ ```
196
+ Parameters:
197
+ - error_message: The error message to search for
198
+ - language: Programming language (e.g., "python", "javascript")
199
+ - technologies: Related technologies (e.g., ["react", "django"])
200
+ - min_score: Minimum score threshold for questions
201
+ - include_comments: Whether to include comments in results
202
+ - response_format: Format of response ("json" or "markdown")
203
+ - limit: Maximum number of results to return
204
+ ```
205
+
206
+ #### 3. get_question
207
+
208
+ Get a specific Stack Overflow question by ID.
209
+
210
+ ```
211
+ Parameters:
212
+ - question_id: The Stack Overflow question ID
213
+ - include_comments: Whether to include comments in results
214
+ - response_format: Format of response ("json" or "markdown")
215
+ ```
216
+
217
+ #### 4. analyze_stack_trace
218
+
219
+ Analyze a stack trace and find relevant solutions on Stack Overflow.
220
+
221
+ ```
222
+ Parameters:
223
+ - stack_trace: The stack trace to analyze
224
+ - language: Programming language of the stack trace
225
+ - include_comments: Whether to include comments in results
226
+ - response_format: Format of response ("json" or "markdown")
227
+ - limit: Maximum number of results to return
228
+ ```
229
+
230
+ #### 5. advanced_search
231
+
232
+ Advanced search for Stack Overflow questions with many filter options.
233
+
234
+ ```
235
+ Parameters:
236
+ - query: Free-form search query
237
+ - tags: List of tags to filter by
238
+ - excluded_tags: List of tags to exclude
239
+ - min_score: Minimum score threshold
240
+ - title: Text that must appear in the title
241
+ - body: Text that must appear in the body
242
+ - answers: Minimum number of answers
243
+ - has_accepted_answer: Whether questions must have an accepted answer
244
+ - sort_by: Field to sort by (activity, creation, votes, relevance)
245
+ - include_comments: Whether to include comments in results
246
+ - response_format: Format of response ("json" or "markdown")
247
+ - limit: Maximum number of results to return
248
+ ```
249
+
250
+ ## Development
251
+
252
+ This section is for contributors who want to develop or extend the Stack Overflow MCP server.
253
+
254
+ ### Setting Up Development Environment
255
+
256
+ ```bash
257
+ # Clone the repository
258
+ git clone https://github.com/yourusername/stackoverflow-mcp-server.git
259
+ cd stackoverflow-mcp-server
260
+
261
+ # Install dev dependencies
262
+ uv pip install -e ".[dev]"
263
+ ```
264
+
265
+ ### Running Tests
266
+
267
+ ```bash
268
+ # Run all tests
269
+ pytest
270
+
271
+ # Run specific test modules
272
+ pytest tests/test_formatter.py
273
+ pytest tests/test_server.py
274
+
275
+ # Run tests with coverage report
276
+ pytest --cov=stackoverflow_mcp
277
+ ```
278
+
279
+ ### Project Structure
280
+
281
+ ```
282
+ stackoverflow-mcp-server/
283
+ ├── stackoverflow_mcp/ # Main package
284
+ │ ├── __init__.py
285
+ | |── __main__.py # Entry point
286
+ │ ├── api.py # Stack Exchange API client
287
+ │ ├── env.py # Environment configuration
288
+ │ ├── formatter.py # Response formatting utilities
289
+ │ ├── server.py # MCP server implementation
290
+ │ └── types.py # Data classes
291
+ ├── tests/ # Test suite
292
+ │ ├── api/
293
+ │ │ └── test_search.py # API search tests
294
+ │ ├── test_formatter.py # Formatter tests
295
+ │ ├── test_general_api_health.py # API health tests
296
+ │ └── test_server.py # Server tests
297
+ ├── pyproject.toml # Package configuration
298
+ ├── api_query.py # testing stackexchange outside of MCP context
299
+ ├── LICENSE # License file
300
+ └── README.md # This file
301
+ ```
302
+
303
+ ## Contributing
304
+
305
+ Contributions are welcome! Here's how you can contribute:
306
+
307
+ 1. Fork the repository
308
+ 2. Create a feature branch: `git checkout -b feature/my-feature`
309
+ 3. Commit your changes: `git commit -am 'Add new feature'`
310
+ 4. Push to the branch: `git push origin feature/my-feature`
311
+ 5. Submit a pull request
312
+
313
+ Please make sure to update tests as appropriate and follow the project's coding style.
314
+
315
+ ## License
316
+
317
+ This project is licensed under the MIT License - see the LICENSE file for details.
318
+
319
+ ---
320
+
321
+ <p align="center">
322
+ Stack Overflow MCP Server: AI-accessible programming knowledge
323
+ </p>
324
+
325
+ <!-- Badges -->
326
 
327
+ [python-badge]: https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue.svg
328
+ [python-url]: https://www.python.org/downloads/
329
+ [license-badge]: https://img.shields.io/badge/license-MIT-green.svg
330
+ [license-url]: LICENSE
README_SPACES.md ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Stack Overflow MCP Server
3
+ emoji: 🔍
4
+ colorFrom: orange
5
+ colorTo: purple
6
+ sdk: gradio
7
+ sdk_version: "5.33.1"
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ ---
12
+
13
+ # Stack Overflow MCP Server 🚀
14
+
15
+ A powerful Gradio app that serves as both a web interface and MCP (Model Context Protocol) server for Stack Overflow search capabilities.
16
+
17
+ ## 🌟 Features
18
+
19
+ - **Web Interface**: Interactive search with 5 specialized tabs
20
+ - **MCP Server**: Expose 5 MCP tools for AI assistants
21
+ - **API Key Support**: Add your Stack Exchange API key for higher quotas
22
+ - **Real-time Search**: Fast and accurate Stack Overflow searches
23
+
24
+ ## 🔧 MCP Integration
25
+
26
+ This app serves as an MCP server at the `/gradio_api/mcp/sse` endpoint. Connect your AI assistant using:
27
+
28
+ ```json
29
+ {
30
+ "mcpServers": {
31
+ "stackoverflow": {
32
+ "url": "https://YOUR_SPACE_URL/gradio_api/mcp/sse"
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
38
+ ## 🎯 Available MCP Tools
39
+
40
+ 1. **search_by_query_sync** - General Stack Overflow search
41
+ 2. **search_by_error_sync** - Error-specific search
42
+ 3. **get_question_sync** - Get specific question by ID
43
+ 4. **analyze_stack_trace_sync** - Analyze stack traces
44
+ 5. **advanced_search_sync** - Advanced search with filters
45
+
46
+ ## 💡 Usage
47
+
48
+ 1. **Web Interface**: Use the tabs to search Stack Overflow
49
+ 2. **MCP Server**: Connect AI assistants to the MCP endpoint
50
+ 3. **API Key**: Add your Stack Exchange API key for 10,000 requests/day
51
+
52
+ Built with ❤️ for the MCP Hackathon
api_query.py ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Stack Exchange API Query Tool
4
+
5
+ This script allows you to directly call the Stack Exchange API with various parameters
6
+ and see the results. It's useful for testing queries and seeing the raw results.
7
+
8
+ Usage:
9
+ python api_query.py search "python pandas dataframe" --tags python,pandas --min-score 10
10
+ python api_query.py question 12345
11
+ python api_query.py error "TypeError: cannot use a string pattern" --language python
12
+ """
13
+
14
+ import os
15
+ import sys
16
+ import json
17
+ import asyncio
18
+ import argparse
19
+ from dotenv import load_dotenv
20
+
21
+ from stackoverflow_mcp.api import StackExchangeAPI
22
+ from stackoverflow_mcp.formatter import format_response
23
+
24
+
25
+ def setup_environment():
26
+ """Load environment variables from .env file"""
27
+ if os.path.exists(".env"):
28
+ load_dotenv(".env")
29
+ elif os.path.exists(".env.test"):
30
+ load_dotenv(".env.test")
31
+ else:
32
+ print("Warning: No .env or .env.test file found. Using default settings.")
33
+
34
+
35
+ async def run_search_query(api, args):
36
+ """Run a search query with the given arguments"""
37
+ tags = args.tags.split(',') if args.tags else None
38
+
39
+ excluded_tags = args.excluded_tags.split(',') if args.excluded_tags else None
40
+
41
+ print(f"\nRunning search query: '{args.query}'")
42
+ if args.title:
43
+ print(f"Running search with title containing: '{args.title}'")
44
+ if args.body:
45
+ print(f"Running search with body containing: '{args.body}'")
46
+ print(f"Tags: {tags}")
47
+ print(f"Excluded tags: {excluded_tags}")
48
+ print(f"Min score: {args.min_score}")
49
+ print(f"Limit: {args.limit}")
50
+ print(f"Include comments: {args.comments}\n")
51
+
52
+ try:
53
+ results = await api.search_by_query(
54
+ query=args.query,
55
+ tags=tags,
56
+ title=args.title,
57
+ body=args.body,
58
+ excluded_tags=excluded_tags,
59
+ min_score=args.min_score,
60
+ limit=args.limit,
61
+ include_comments=args.comments
62
+ )
63
+
64
+ print(f"Found {len(results)} results")
65
+
66
+ if args.raw:
67
+ for i, result in enumerate(results):
68
+ print(f"\n--- Result {i+1} ---")
69
+ print(f"Question ID: {result.question.question_id}")
70
+ print(f"Title: {result.question.title}")
71
+ print(f"Score: {result.question.score}")
72
+ print(f"Tags: {result.question.tags}")
73
+ print(f"Link: {result.question.link}")
74
+ print(f"Answers: {len(result.answers)}")
75
+ if result.comments:
76
+ print(f"Question comments: {len(result.comments.question)}")
77
+ else:
78
+ formatted = format_response(results, args.format)
79
+ print(formatted)
80
+
81
+ except Exception as e:
82
+ print(f"Error during search: {str(e)}")
83
+
84
+
85
+ async def run_question_query(api, args):
86
+ """Get a specific question by ID"""
87
+ try:
88
+ print(f"\nFetching question ID: {args.question_id}")
89
+ print(f"Include comments: {args.comments}\n")
90
+
91
+ result = await api.get_question(
92
+ question_id=args.question_id,
93
+ include_comments=args.comments
94
+ )
95
+
96
+ if args.raw:
97
+ print(f"Question ID: {result.question.question_id}")
98
+ print(f"Title: {result.question.title}")
99
+ print(f"Score: {result.question.score}")
100
+ print(f"Tags: {result.question.tags}")
101
+ print(f"Link: {result.question.link}")
102
+ print(f"Answers: {len(result.answers)}")
103
+ if result.comments:
104
+ print(f"Question comments: {len(result.comments.question)}")
105
+ else:
106
+ formatted = format_response([result], args.format)
107
+ print(formatted)
108
+
109
+ except Exception as e:
110
+ print(f"Error fetching question: {str(e)}")
111
+
112
+
113
+ async def run_error_query(api, args):
114
+ """Search for an error message with optional language filter"""
115
+ technologies = args.technologies.split(',') if args.technologies else None
116
+
117
+ try:
118
+ print(f"\nSearching for error: '{args.error}'")
119
+ print(f"Language: {args.language}")
120
+ print(f"Technologies: {technologies}")
121
+ if args.title:
122
+ print(f"Title containing: '{args.title}'")
123
+ if args.body:
124
+ print(f"Body containing: '{args.body}'")
125
+ print(f"Min score: {args.min_score}")
126
+ print(f"Limit: {args.limit}")
127
+ print(f"Include comments: {args.comments}\n")
128
+
129
+ tags = []
130
+ if args.language:
131
+ tags.append(args.language.lower())
132
+ if technologies:
133
+ tags.extend([t.lower() for t in technologies])
134
+
135
+ results = await api.search_by_query(
136
+ query=args.error,
137
+ title=args.title,
138
+ body=args.body,
139
+ tags=tags if tags else None,
140
+ min_score=args.min_score,
141
+ limit=args.limit,
142
+ include_comments=args.comments
143
+ )
144
+
145
+ print(f"Found {len(results)} results")
146
+
147
+ if args.raw:
148
+ for i, result in enumerate(results):
149
+ print(f"\n--- Result {i+1} ---")
150
+ print(f"Question ID: {result.question.question_id}")
151
+ print(f"Title: {result.question.title}")
152
+ print(f"Score: {result.question.score}")
153
+ print(f"Tags: {result.question.tags}")
154
+ print(f"Link: {result.question.link}")
155
+ print(f"Answers: {len(result.answers)}")
156
+ if result.comments:
157
+ print(f"Question comments: {len(result.comments.question)}")
158
+ else:
159
+ formatted = format_response(results, args.format)
160
+ print(formatted)
161
+
162
+ except Exception as e:
163
+ print(f"Error searching for error: {str(e)}")
164
+
165
+
166
+ async def main():
167
+ """Parse arguments and run the appropriate query"""
168
+ parser = argparse.ArgumentParser(description="Stack Exchange API Query Tool")
169
+ subparsers = parser.add_subparsers(dest="command", help="Command to run")
170
+
171
+ # Search command
172
+ search_parser = subparsers.add_parser("search", help="Search Stack Overflow")
173
+ search_parser.add_argument("query", help="Search query")
174
+ search_parser.add_argument("--tags", help="Comma-separated list of tags")
175
+ search_parser.add_argument("--title", help="Word(s) that must appear in the question title")
176
+ search_parser.add_argument("--body", help="Word(s) that must appear in the body of the question")
177
+ search_parser.add_argument("--excluded-tags", help="Comma-separated list of tags to exclude")
178
+ search_parser.add_argument("--min-score", type=int, default=0, help="Minimum score")
179
+ search_parser.add_argument("--limit", type=int, default=5, help="Maximum number of results")
180
+ search_parser.add_argument("--comments", action="store_true", help="Include comments")
181
+ search_parser.add_argument("--format", choices=["markdown", "json"], default="markdown", help="Output format")
182
+ search_parser.add_argument("--raw", action="store_true", help="Print raw data structure")
183
+
184
+ # Question command
185
+ question_parser = subparsers.add_parser("question", help="Get a specific question")
186
+ question_parser.add_argument("question_id", type=int, help="Question ID")
187
+ question_parser.add_argument("--comments", action="store_true", help="Include comments")
188
+ question_parser.add_argument("--format", choices=["markdown", "json"], default="markdown", help="Output format")
189
+ question_parser.add_argument("--raw", action="store_true", help="Print raw data structure")
190
+
191
+ # Error command
192
+ error_parser = subparsers.add_parser("error", help="Search for an error message")
193
+ error_parser.add_argument("error", help="Error message")
194
+ error_parser.add_argument("--title", help="Word(s) that must appear in the question title")
195
+ error_parser.add_argument("--body", help="Word(s) that must appear in the body of the question")
196
+ error_parser.add_argument("--language", help="Programming language")
197
+ error_parser.add_argument("--technologies", help="Comma-separated list of technologies")
198
+ error_parser.add_argument("--min-score", type=int, default=0, help="Minimum score")
199
+ error_parser.add_argument("--limit", type=int, default=5, help="Maximum number of results")
200
+ error_parser.add_argument("--comments", action="store_true", help="Include comments")
201
+ error_parser.add_argument("--format", choices=["markdown", "json"], default="markdown", help="Output format")
202
+ error_parser.add_argument("--raw", action="store_true", help="Print raw data structure")
203
+
204
+ args = parser.parse_args()
205
+
206
+ if not args.command:
207
+ parser.print_help()
208
+ return 1
209
+
210
+ setup_environment()
211
+
212
+ api_key = os.getenv("STACK_EXCHANGE_API_KEY")
213
+
214
+ if not api_key:
215
+ print("Warning: No API key found. Requests may be rate limited.")
216
+
217
+ api = StackExchangeAPI(api_key=api_key)
218
+
219
+ try:
220
+ if args.command == "search":
221
+ await run_search_query(api, args)
222
+ elif args.command == "question":
223
+ await run_question_query(api, args)
224
+ elif args.command == "error":
225
+ await run_error_query(api, args)
226
+
227
+ except Exception as e:
228
+ print(f"Error: {str(e)}")
229
+ return 1
230
+
231
+ finally:
232
+ await api.close()
233
+
234
+ return 0
235
+
236
+
237
+ if __name__ == "__main__":
238
+ sys.exit(asyncio.run(main()))
app.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Stack Overflow MCP Server - Hugging Face Spaces App
4
+ A web interface and MCP server that provides Stack Overflow search capabilities.
5
+ """
6
+
7
+ # Import everything from the main gradio_app
8
+ from gradio_app import *
9
+
10
+ if __name__ == "__main__":
11
+ # Launch the app for Hugging Face Spaces
12
+ demo.launch(
13
+ mcp_server=True,
14
+ server_name="0.0.0.0",
15
+ server_port=7860,
16
+ show_error=True,
17
+ share=False
18
+ )
demo_mcp_client.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ MCP Client Demo for Stack Overflow Server
4
+ Demonstrates how to interact with the Stack Overflow MCP server programmatically.
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ from typing import Dict, Any
10
+
11
+ # This is a demonstration of how an MCP client would interact with our server
12
+ # In practice, you would use a proper MCP client library
13
+
14
+ async def demo_mcp_calls():
15
+ """
16
+ Demonstrate various MCP calls to the Stack Overflow server.
17
+ This simulates what an AI assistant like Claude would do.
18
+ """
19
+
20
+ print("🤖 Stack Overflow MCP Server Demo")
21
+ print("=" * 50)
22
+
23
+ # Demo 1: General Search
24
+ print("\n1️⃣ GENERAL SEARCH DEMO")
25
+ print("Query: 'Django pagination best practices'")
26
+ print("Tags: ['python', 'django']")
27
+ print("Expected: High-quality Django pagination solutions")
28
+
29
+ # Demo 2: Error Search
30
+ print("\n2️⃣ ERROR SEARCH DEMO")
31
+ print("Error: 'TypeError: NoneType object has no attribute'")
32
+ print("Language: Python")
33
+ print("Expected: Common solutions for NoneType errors")
34
+
35
+ # Demo 3: Question Retrieval
36
+ print("\n3️⃣ QUESTION RETRIEVAL DEMO")
37
+ print("Question ID: 11227809")
38
+ print("Expected: Famous 'Why is processing a sorted array faster?' question")
39
+
40
+ # Demo 4: Stack Trace Analysis
41
+ print("\n4️⃣ STACK TRACE ANALYSIS DEMO")
42
+ print("Stack Trace: 'ReferenceError: useState is not defined'")
43
+ print("Language: JavaScript")
44
+ print("Expected: React hooks solutions")
45
+
46
+ # Demo 5: Advanced Search
47
+ print("\n5️⃣ ADVANCED SEARCH DEMO")
48
+ print("Query: 'memory optimization'")
49
+ print("Tags: ['c++', 'performance']")
50
+ print("Min Score: 50")
51
+ print("Expected: High-quality C++ performance answers")
52
+
53
+ print("\n" + "=" * 50)
54
+ print("🎯 All demos completed!")
55
+ print("💡 These are the types of searches our MCP server can handle")
56
+ print("🚀 Try them in the web interface or via MCP client!")
57
+
58
+ def demo_gradio_api_calls():
59
+ """
60
+ Demonstrate how the Gradio API exposes the MCP functionality.
61
+ """
62
+
63
+ print("\n🎨 GRADIO API INTEGRATION")
64
+ print("=" * 50)
65
+
66
+ # Show the available API endpoints
67
+ api_endpoints = {
68
+ "search_by_query_sync": {
69
+ "description": "General Stack Overflow search",
70
+ "inputs": ["query", "tags", "min_score", "has_accepted_answer", "limit", "response_format"],
71
+ "example": {
72
+ "query": "Django pagination best practices",
73
+ "tags": "python,django",
74
+ "min_score": 5,
75
+ "has_accepted_answer": True,
76
+ "limit": 5,
77
+ "response_format": "markdown"
78
+ }
79
+ },
80
+ "search_by_error_sync": {
81
+ "description": "Error-specific search",
82
+ "inputs": ["error_message", "language", "technologies", "min_score", "has_accepted_answer", "limit", "response_format"],
83
+ "example": {
84
+ "error_message": "TypeError: 'NoneType' object has no attribute",
85
+ "language": "python",
86
+ "technologies": "flask,sqlalchemy",
87
+ "min_score": 0,
88
+ "has_accepted_answer": True,
89
+ "limit": 5,
90
+ "response_format": "markdown"
91
+ }
92
+ },
93
+ "get_question_sync": {
94
+ "description": "Get specific question by ID",
95
+ "inputs": ["question_id", "include_comments", "response_format"],
96
+ "example": {
97
+ "question_id": "11227809",
98
+ "include_comments": True,
99
+ "response_format": "markdown"
100
+ }
101
+ },
102
+ "analyze_stack_trace_sync": {
103
+ "description": "Analyze stack traces",
104
+ "inputs": ["stack_trace", "language", "min_score", "has_accepted_answer", "limit", "response_format"],
105
+ "example": {
106
+ "stack_trace": "ReferenceError: useState is not defined\n at Component.render",
107
+ "language": "javascript",
108
+ "min_score": 5,
109
+ "has_accepted_answer": True,
110
+ "limit": 3,
111
+ "response_format": "markdown"
112
+ }
113
+ },
114
+ "advanced_search_sync": {
115
+ "description": "Advanced search with comprehensive filters",
116
+ "inputs": ["query", "tags", "excluded_tags", "min_score", "title", "body", "min_answers", "has_accepted_answer", "min_views", "sort_by", "limit", "response_format"],
117
+ "example": {
118
+ "query": "memory optimization",
119
+ "tags": "c++,performance",
120
+ "excluded_tags": "beginner",
121
+ "min_score": 50,
122
+ "title": "",
123
+ "body": "",
124
+ "min_answers": 1,
125
+ "has_accepted_answer": False,
126
+ "min_views": 1000,
127
+ "sort_by": "votes",
128
+ "limit": 5,
129
+ "response_format": "markdown"
130
+ }
131
+ }
132
+ }
133
+
134
+ for endpoint, info in api_endpoints.items():
135
+ print(f"\n🔧 {endpoint}")
136
+ print(f" 📝 {info['description']}")
137
+ print(f" 📊 Example:")
138
+ for key, value in info['example'].items():
139
+ print(f" {key}: {value}")
140
+
141
+ def demo_mcp_integration():
142
+ """
143
+ Show how to integrate with MCP clients like Claude Desktop.
144
+ """
145
+
146
+ print("\n🔗 MCP CLIENT INTEGRATION")
147
+ print("=" * 50)
148
+ {
149
+
150
+ }
151
+
152
+ claude_config = {
153
+ "mcpServers": {
154
+ "stackoverflow": {
155
+ "command": "npx",
156
+ "args": [
157
+ "mcp-remote",
158
+ "https://c44b366466c774a9d5.gradio.live/gradio_api/mcp/sse"
159
+ ]
160
+ }
161
+ }
162
+ }
163
+
164
+ print("💻 Claude Desktop Configuration:")
165
+ print(json.dumps(claude_config, indent=2))
166
+
167
+ print("\n🤖 Example AI Assistant Prompts:")
168
+ prompts = [
169
+ "Search Stack Overflow for Django pagination best practices",
170
+ "Find solutions for the error 'TypeError: NoneType object has no attribute'",
171
+ "Get Stack Overflow question 11227809",
172
+ "Analyze this JavaScript error: ReferenceError: useState is not defined",
173
+ "Find high-scored C++ memory optimization questions"
174
+ ]
175
+
176
+ for i, prompt in enumerate(prompts, 1):
177
+ print(f" {i}. {prompt}")
178
+
179
+ if __name__ == "__main__":
180
+ print("🚀 Starting Stack Overflow MCP Server Demo...")
181
+
182
+ # Run the async demo
183
+ asyncio.run(demo_mcp_calls())
184
+
185
+ # Show Gradio API integration
186
+ demo_gradio_api_calls()
187
+
188
+ # Show MCP client integration
189
+ demo_mcp_integration()
190
+
191
+ print("\n🎯 Demo completed! Visit the web interface to try it live:")
192
+ print("🌐 https://c44b366466c774a9d5.gradio.live")
193
+ print("🔗 MCP Endpoint: https://c44b366466c774a9d5.gradio.live/gradio_api/mcp/sse")
gradio_app.py ADDED
@@ -0,0 +1,810 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Gradio MCP Server for Stack Overflow Search
4
+ A web interface and MCP server that provides Stack Overflow search capabilities.
5
+ """
6
+
7
+ import asyncio
8
+ import os
9
+ from typing import List, Optional, Tuple
10
+ import gradio as gr
11
+ from datetime import datetime
12
+
13
+ from stackoverflow_mcp.api import StackExchangeAPI
14
+ from stackoverflow_mcp.formatter import format_response
15
+ from stackoverflow_mcp.env import STACK_EXCHANGE_API_KEY
16
+
17
+ # Initialize a default API client (can be overridden with user's key)
18
+ default_api = StackExchangeAPI(api_key=STACK_EXCHANGE_API_KEY)
19
+
20
+ def get_api_client(api_key: str = "") -> StackExchangeAPI:
21
+ """Get API client with user's key or fallback to default."""
22
+ if api_key and api_key.strip():
23
+ return StackExchangeAPI(api_key=api_key.strip())
24
+ return default_api
25
+
26
+ def search_by_query_sync(
27
+ query: str,
28
+ tags: str = "",
29
+ min_score: int = 0,
30
+ has_accepted_answer: bool = False,
31
+ limit: int = 5,
32
+ response_format: str = "markdown",
33
+ api_key: str = ""
34
+ ) -> str:
35
+ """
36
+ Search Stack Overflow for questions matching a query.
37
+
38
+ Args:
39
+ query (str): The search query
40
+ tags (str): Comma-separated list of tags to filter by (e.g., "python,pandas")
41
+ min_score (int): Minimum score threshold for questions
42
+ has_accepted_answer (bool): Whether questions must have an accepted answer
43
+ limit (int): Maximum number of results to return (1-20)
44
+ response_format (str): Format of response ("json" or "markdown")
45
+
46
+ Returns:
47
+ str: Formatted search results
48
+ """
49
+ if not query.strip():
50
+ return "❌ Please enter a search query."
51
+
52
+ # Convert tags string to list
53
+ tags_list = [tag.strip() for tag in tags.split(",") if tag.strip()] if tags else None
54
+
55
+ # Limit the range
56
+ limit = max(1, min(limit, 20))
57
+
58
+ try:
59
+ # Get API client with user's key
60
+ api = get_api_client(api_key)
61
+
62
+ # Run the async function safely
63
+ try:
64
+ loop = asyncio.get_event_loop()
65
+ if loop.is_closed():
66
+ raise RuntimeError("Event loop is closed")
67
+ except RuntimeError:
68
+ loop = asyncio.new_event_loop()
69
+ asyncio.set_event_loop(loop)
70
+
71
+ results = loop.run_until_complete(
72
+ api.search_by_query(
73
+ query=query,
74
+ tags=tags_list,
75
+ min_score=min_score if min_score > 0 else None,
76
+ has_accepted_answer=has_accepted_answer if has_accepted_answer else None,
77
+ limit=limit
78
+ )
79
+ )
80
+
81
+ if not results:
82
+ return f"🔍 No results found for query: '{query}'"
83
+
84
+ return format_response(results, response_format)
85
+
86
+ except Exception as e:
87
+ return f"❌ Error searching Stack Overflow: {str(e)}"
88
+
89
+
90
+ def search_by_error_sync(
91
+ error_message: str,
92
+ language: str = "",
93
+ technologies: str = "",
94
+ min_score: int = 0,
95
+ has_accepted_answer: bool = False,
96
+ limit: int = 5,
97
+ response_format: str = "markdown",
98
+ api_key: str = ""
99
+ ) -> str:
100
+ """
101
+ Search Stack Overflow for solutions to an error message.
102
+
103
+ Args:
104
+ error_message (str): The error message to search for
105
+ language (str): Programming language (e.g., "python", "javascript")
106
+ technologies (str): Comma-separated related technologies (e.g., "react,django")
107
+ min_score (int): Minimum score threshold for questions
108
+ has_accepted_answer (bool): Whether questions must have an accepted answer
109
+ limit (int): Maximum number of results to return (1-20)
110
+ response_format (str): Format of response ("json" or "markdown")
111
+
112
+ Returns:
113
+ str: Formatted search results
114
+ """
115
+ if not error_message.strip():
116
+ return "❌ Please enter an error message."
117
+
118
+ # Build tags list
119
+ tags = []
120
+ if language.strip():
121
+ tags.append(language.strip().lower())
122
+ if technologies.strip():
123
+ tags.extend([tech.strip().lower() for tech in technologies.split(",") if tech.strip()])
124
+
125
+ # Limit the range
126
+ limit = max(1, min(limit, 20))
127
+
128
+ try:
129
+ # Get API client with user's key
130
+ api = get_api_client(api_key)
131
+
132
+ # Run the async function safely
133
+ try:
134
+ loop = asyncio.get_event_loop()
135
+ if loop.is_closed():
136
+ raise RuntimeError("Event loop is closed")
137
+ except RuntimeError:
138
+ loop = asyncio.new_event_loop()
139
+ asyncio.set_event_loop(loop)
140
+
141
+ results = loop.run_until_complete(
142
+ api.search_by_query(
143
+ query=error_message,
144
+ tags=tags if tags else None,
145
+ min_score=min_score if min_score > 0 else None,
146
+ has_accepted_answer=has_accepted_answer if has_accepted_answer else None,
147
+ limit=limit
148
+ )
149
+ )
150
+
151
+ if not results:
152
+ return f"🔍 No results found for error: '{error_message}'"
153
+
154
+ return format_response(results, response_format)
155
+
156
+ except Exception as e:
157
+ return f"❌ Error searching Stack Overflow: {str(e)}"
158
+
159
+
160
+ def get_question_sync(
161
+ question_id: str,
162
+ include_comments: bool = True,
163
+ response_format: str = "markdown",
164
+ api_key: str = ""
165
+ ) -> str:
166
+ """
167
+ Get a specific Stack Overflow question by ID.
168
+
169
+ Args:
170
+ question_id (str): The Stack Overflow question ID
171
+ include_comments (bool): Whether to include comments in results
172
+ response_format (str): Format of response ("json" or "markdown")
173
+
174
+ Returns:
175
+ str: Formatted question details
176
+ """
177
+ if not question_id.strip():
178
+ return "❌ Please enter a question ID."
179
+
180
+ try:
181
+ # Convert to int
182
+ q_id = int(question_id.strip())
183
+
184
+ # Get API client with user's key
185
+ api = get_api_client(api_key)
186
+
187
+ # Run the async function safely
188
+ try:
189
+ loop = asyncio.get_event_loop()
190
+ if loop.is_closed():
191
+ raise RuntimeError("Event loop is closed")
192
+ except RuntimeError:
193
+ loop = asyncio.new_event_loop()
194
+ asyncio.set_event_loop(loop)
195
+
196
+ result = loop.run_until_complete(
197
+ api.get_question(
198
+ question_id=q_id,
199
+ include_comments=include_comments
200
+ )
201
+ )
202
+
203
+ return format_response([result], response_format)
204
+
205
+ except ValueError:
206
+ return "❌ Question ID must be a number."
207
+ except Exception as e:
208
+ return f"❌ Error fetching question: {str(e)}"
209
+
210
+
211
+ def analyze_stack_trace_sync(
212
+ stack_trace: str,
213
+ language: str,
214
+ min_score: int = 0,
215
+ has_accepted_answer: bool = False,
216
+ limit: int = 3,
217
+ response_format: str = "markdown",
218
+ api_key: str = ""
219
+ ) -> str:
220
+ """
221
+ Analyze a stack trace and find relevant solutions on Stack Overflow.
222
+
223
+ Args:
224
+ stack_trace (str): The stack trace to analyze
225
+ language (str): Programming language of the stack trace
226
+ min_score (int): Minimum score threshold for questions
227
+ has_accepted_answer (bool): Whether questions must have an accepted answer
228
+ limit (int): Maximum number of results to return (1-10)
229
+ response_format (str): Format of response ("json" or "markdown")
230
+
231
+ Returns:
232
+ str: Formatted search results
233
+ """
234
+ if not stack_trace.strip():
235
+ return "❌ Please enter a stack trace."
236
+
237
+ if not language.strip():
238
+ return "❌ Please specify the programming language."
239
+
240
+ # Limit the range
241
+ limit = max(1, min(limit, 10))
242
+
243
+ # Extract the first line as the main error
244
+ error_lines = stack_trace.strip().split("\n")
245
+ error_message = error_lines[0]
246
+
247
+ try:
248
+ # Get API client with user's key
249
+ api = get_api_client(api_key)
250
+
251
+ # Run the async function safely
252
+ try:
253
+ loop = asyncio.get_event_loop()
254
+ if loop.is_closed():
255
+ raise RuntimeError("Event loop is closed")
256
+ except RuntimeError:
257
+ loop = asyncio.new_event_loop()
258
+ asyncio.set_event_loop(loop)
259
+
260
+ results = loop.run_until_complete(
261
+ api.search_by_query(
262
+ query=error_message,
263
+ tags=[language.strip().lower()],
264
+ min_score=min_score if min_score > 0 else None,
265
+ has_accepted_answer=has_accepted_answer if has_accepted_answer else None,
266
+ limit=limit
267
+ )
268
+ )
269
+
270
+ if not results:
271
+ return f"🔍 No results found for stack trace error: '{error_message}'"
272
+
273
+ return format_response(results, response_format)
274
+
275
+ except Exception as e:
276
+ return f"❌ Error analyzing stack trace: {str(e)}"
277
+
278
+
279
+ def advanced_search_sync(
280
+ query: str = "",
281
+ tags: str = "",
282
+ excluded_tags: str = "",
283
+ min_score: int = 0,
284
+ title: str = "",
285
+ body: str = "",
286
+ min_answers: int = 0,
287
+ has_accepted_answer: bool = False,
288
+ min_views: int = 0,
289
+ sort_by: str = "votes",
290
+ limit: int = 5,
291
+ response_format: str = "markdown",
292
+ api_key: str = ""
293
+ ) -> str:
294
+ """
295
+ Advanced search for Stack Overflow questions with comprehensive filters.
296
+
297
+ Args:
298
+ query (str): Free-form search query
299
+ tags (str): Comma-separated list of tags to filter by
300
+ excluded_tags (str): Comma-separated list of tags to exclude
301
+ min_score (int): Minimum score threshold
302
+ title (str): Text that must appear in the title
303
+ body (str): Text that must appear in the body
304
+ min_answers (int): Minimum number of answers
305
+ has_accepted_answer (bool): Whether questions must have an accepted answer
306
+ min_views (int): Minimum number of views
307
+ sort_by (str): Field to sort by (activity, creation, votes, relevance)
308
+ limit (int): Maximum number of results to return (1-20)
309
+ response_format (str): Format of response ("json" or "markdown")
310
+
311
+ Returns:
312
+ str: Formatted search results
313
+ """
314
+ if not query.strip() and not tags.strip() and not title.strip() and not body.strip():
315
+ return "❌ Please provide at least one search criteria (query, tags, title, or body)."
316
+
317
+ # Convert tags strings to lists
318
+ tags_list = [tag.strip() for tag in tags.split(",") if tag.strip()] if tags else None
319
+ excluded_tags_list = [tag.strip() for tag in excluded_tags.split(",") if tag.strip()] if excluded_tags else None
320
+
321
+ # Limit the range
322
+ limit = max(1, min(limit, 20))
323
+
324
+ try:
325
+ # Get API client with user's key
326
+ api = get_api_client(api_key)
327
+
328
+ # Run the async function safely
329
+ try:
330
+ loop = asyncio.get_event_loop()
331
+ if loop.is_closed():
332
+ raise RuntimeError("Event loop is closed")
333
+ except RuntimeError:
334
+ loop = asyncio.new_event_loop()
335
+ asyncio.set_event_loop(loop)
336
+
337
+ results = loop.run_until_complete(
338
+ api.advanced_search(
339
+ query=query.strip() if query.strip() else None,
340
+ tags=tags_list,
341
+ excluded_tags=excluded_tags_list,
342
+ min_score=min_score if min_score > 0 else None,
343
+ title=title.strip() if title.strip() else None,
344
+ body=body.strip() if body.strip() else None,
345
+ answers=min_answers if min_answers > 0 else None,
346
+ has_accepted_answer=has_accepted_answer if has_accepted_answer else None,
347
+ views=min_views if min_views > 0 else None,
348
+ sort_by=sort_by,
349
+ limit=limit
350
+ )
351
+ )
352
+
353
+ if not results:
354
+ return "🔍 No results found with the specified criteria."
355
+
356
+ return format_response(results, response_format)
357
+
358
+ except Exception as e:
359
+ return f"❌ Error performing advanced search: {str(e)}"
360
+
361
+
362
+ # Helper functions for example buttons (not exposed as MCP tools)
363
+ def _set_django_example():
364
+ return ("Django pagination best practices", "python,django", 5, True, 5, "markdown")
365
+
366
+ def _set_async_example():
367
+ return ("Python asyncio concurrency patterns", "python,asyncio", 10, True, 5, "markdown")
368
+
369
+ def _set_react_example():
370
+ return ("React hooks useState useEffect", "javascript,reactjs", 15, True, 5, "markdown")
371
+
372
+ def _set_sql_example():
373
+ return ("SQL INNER JOIN vs LEFT JOIN performance", "sql,join", 20, True, 5, "markdown")
374
+
375
+
376
+ # Create the Gradio interface with multiple tabs
377
+ with gr.Blocks(
378
+ title="Stack Overflow MCP Server",
379
+ theme=gr.themes.Soft(),
380
+ css="""
381
+ .gradio-container {
382
+ max-width: 1200px !important;
383
+ }
384
+ .tab-nav button {
385
+ font-size: 16px !important;
386
+ }
387
+ """
388
+ ) as demo:
389
+
390
+ gr.Markdown("""
391
+ # 🔍 Stack Overflow MCP Server
392
+
393
+ **A powerful interface to search Stack Overflow and analyze programming errors**
394
+
395
+ This application serves as both a web interface and an MCP (Model Context Protocol) server,
396
+ allowing AI assistants like Claude to search Stack Overflow programmatically.
397
+
398
+ 💡 **MCP Server URL**: Use this URL in your MCP client: `{SERVER_URL}/gradio_api/mcp/sse`
399
+
400
+ ## 🚀 Quick Start Examples
401
+
402
+ Try these example searches to get started:
403
+ - **General Search**: "Django pagination best practices" with tags "python,django"
404
+ - **Error Search**: "TypeError: 'NoneType' object has no attribute" in Python
405
+ - **Question ID**: 11227809 (famous "Why is processing a sorted array faster?" question)
406
+ - **Stack Trace**: JavaScript TypeError examples
407
+ - **Advanced**: High-scored Python questions with accepted answers
408
+ """)
409
+
410
+ # Global API Key Input
411
+ with gr.Row():
412
+ with gr.Column(scale=3):
413
+ gr.Markdown("### 🔑 Stack Exchange API Key (Optional)")
414
+ gr.Markdown("""
415
+ **Why provide an API key?**
416
+ - Higher request quotas (10,000 vs 300 requests/day)
417
+ - Faster responses and better reliability
418
+ - API keys are **not secret** - safe to share publicly
419
+
420
+ **How to get one:**
421
+ 1. Visit [Stack Apps OAuth Registration](https://stackapps.com/apps/oauth/register)
422
+ 2. Fill in basic info (name: "Stack Overflow MCP", domain: "localhost")
423
+ 3. Copy your API key from the results page
424
+ """)
425
+
426
+ with gr.Column(scale=2):
427
+ api_key_input = gr.Textbox(
428
+ label="Stack Exchange API Key",
429
+ placeholder="Enter your API key here (optional)",
430
+ value="",
431
+ type="password",
432
+ info="Optional: Provides higher quotas and better performance"
433
+ )
434
+
435
+ with gr.Tabs():
436
+
437
+ # Tab 1: General Search
438
+ with gr.Tab("🔍 General Search", id="search"):
439
+ gr.Markdown("### Search Stack Overflow by query and filters")
440
+
441
+ with gr.Row():
442
+ with gr.Column(scale=2):
443
+ query_input = gr.Textbox(
444
+ label="Search Query",
445
+ placeholder="e.g., 'Django pagination best practices'",
446
+ value="python list comprehension"
447
+ )
448
+
449
+ with gr.Column(scale=1):
450
+ tags_input = gr.Textbox(
451
+ label="Tags (comma-separated)",
452
+ placeholder="e.g., python,pandas",
453
+ value=""
454
+ )
455
+
456
+ with gr.Row():
457
+ min_score_input = gr.Slider(
458
+ label="Minimum Score",
459
+ minimum=0,
460
+ maximum=100,
461
+ value=0,
462
+ step=1
463
+ )
464
+
465
+ has_accepted_input = gr.Checkbox(
466
+ label="Must have accepted answer",
467
+ value=False
468
+ )
469
+
470
+ limit_input = gr.Slider(
471
+ label="Number of Results",
472
+ minimum=1,
473
+ maximum=20,
474
+ value=5,
475
+ step=1
476
+ )
477
+
478
+ format_input = gr.Dropdown(
479
+ label="Response Format",
480
+ choices=["markdown", "json"],
481
+ value="markdown"
482
+ )
483
+
484
+ search_btn = gr.Button("🔍 Search", variant="primary", size="lg")
485
+
486
+ # Example buttons
487
+ with gr.Row():
488
+ gr.Markdown("**Quick Examples:**")
489
+ with gr.Row():
490
+ example1_btn = gr.Button("Django Pagination", size="sm")
491
+ example2_btn = gr.Button("Python Async", size="sm")
492
+ example3_btn = gr.Button("React Hooks", size="sm")
493
+ example4_btn = gr.Button("SQL JOIN", size="sm")
494
+
495
+ # Example button click handlers
496
+ example1_btn.click(
497
+ lambda: ("Django pagination best practices", "python,django", 5, True, 5, "markdown"),
498
+ outputs=[query_input, tags_input, min_score_input, has_accepted_input, limit_input, format_input]
499
+ )
500
+ example2_btn.click(
501
+ lambda: ("Python asyncio concurrency patterns", "python,asyncio", 10, True, 5, "markdown"),
502
+ outputs=[query_input, tags_input, min_score_input, has_accepted_input, limit_input, format_input]
503
+ )
504
+ example3_btn.click(
505
+ lambda: ("React hooks useState useEffect", "javascript,reactjs", 15, True, 5, "markdown"),
506
+ outputs=[query_input, tags_input, min_score_input, has_accepted_input, limit_input, format_input]
507
+ )
508
+ example4_btn.click(
509
+ lambda: ("SQL INNER JOIN vs LEFT JOIN performance", "sql,join", 20, True, 5, "markdown"),
510
+ outputs=[query_input, tags_input, min_score_input, has_accepted_input, limit_input, format_input]
511
+ )
512
+
513
+ search_output = gr.Markdown(label="Search Results", height=400)
514
+
515
+ search_btn.click(
516
+ fn=search_by_query_sync,
517
+ inputs=[query_input, tags_input, min_score_input, has_accepted_input, limit_input, format_input, api_key_input],
518
+ outputs=search_output
519
+ )
520
+
521
+ # Tab 2: Error Search
522
+ with gr.Tab("🐛 Error Search", id="error"):
523
+ gr.Markdown("### Find solutions for specific error messages")
524
+
525
+ with gr.Row():
526
+ with gr.Column(scale=2):
527
+ error_input = gr.Textbox(
528
+ label="Error Message",
529
+ placeholder="e.g., 'TypeError: object of type 'NoneType' has no len()'",
530
+ value="TypeError: 'NoneType' object has no attribute"
531
+ )
532
+
533
+ with gr.Column(scale=1):
534
+ language_input = gr.Textbox(
535
+ label="Programming Language",
536
+ placeholder="e.g., python",
537
+ value="python"
538
+ )
539
+
540
+ tech_input = gr.Textbox(
541
+ label="Related Technologies (comma-separated)",
542
+ placeholder="e.g., django,flask",
543
+ value=""
544
+ )
545
+
546
+ with gr.Row():
547
+ error_min_score = gr.Slider(
548
+ label="Minimum Score",
549
+ minimum=0,
550
+ maximum=100,
551
+ value=0,
552
+ step=1
553
+ )
554
+
555
+ error_accepted = gr.Checkbox(
556
+ label="Must have accepted answer",
557
+ value=True
558
+ )
559
+
560
+ error_limit = gr.Slider(
561
+ label="Number of Results",
562
+ minimum=1,
563
+ maximum=20,
564
+ value=5,
565
+ step=1
566
+ )
567
+
568
+ error_format = gr.Dropdown(
569
+ label="Response Format",
570
+ choices=["markdown", "json"],
571
+ value="markdown"
572
+ )
573
+
574
+ error_search_btn = gr.Button("🐛 Search for Solutions", variant="primary", size="lg")
575
+ error_output = gr.Markdown(label="Error Solutions", height=400)
576
+
577
+ error_search_btn.click(
578
+ fn=search_by_error_sync,
579
+ inputs=[error_input, language_input, tech_input, error_min_score, error_accepted, error_limit, error_format, api_key_input],
580
+ outputs=error_output
581
+ )
582
+
583
+ # Tab 3: Get Specific Question
584
+ with gr.Tab("📄 Get Question", id="question"):
585
+ gr.Markdown("### Retrieve a specific Stack Overflow question by ID")
586
+
587
+ with gr.Row():
588
+ question_id_input = gr.Textbox(
589
+ label="Question ID",
590
+ placeholder="e.g., 11227809",
591
+ value="11227809"
592
+ )
593
+
594
+ question_comments = gr.Checkbox(
595
+ label="Include Comments",
596
+ value=True
597
+ )
598
+
599
+ question_format = gr.Dropdown(
600
+ label="Response Format",
601
+ choices=["markdown", "json"],
602
+ value="markdown"
603
+ )
604
+
605
+ question_btn = gr.Button("📄 Get Question", variant="primary", size="lg")
606
+ question_output = gr.Markdown(label="Question Details", height=400)
607
+
608
+ question_btn.click(
609
+ fn=get_question_sync,
610
+ inputs=[question_id_input, question_comments, question_format, api_key_input],
611
+ outputs=question_output
612
+ )
613
+
614
+ # Tab 4: Stack Trace Analysis
615
+ with gr.Tab("📊 Stack Trace Analysis", id="trace"):
616
+ gr.Markdown("### Analyze stack traces and find relevant solutions")
617
+
618
+ stack_trace_input = gr.Textbox(
619
+ label="Stack Trace",
620
+ placeholder="Paste your full stack trace here...",
621
+ lines=8,
622
+ value="TypeError: Cannot read property 'length' of undefined\n at Array.map (<anonymous>)\n at Component.render (app.js:42:18)"
623
+ )
624
+
625
+ with gr.Row():
626
+ trace_language = gr.Textbox(
627
+ label="Programming Language",
628
+ placeholder="e.g., javascript",
629
+ value="javascript"
630
+ )
631
+
632
+ trace_min_score = gr.Slider(
633
+ label="Minimum Score",
634
+ minimum=0,
635
+ maximum=100,
636
+ value=5,
637
+ step=1
638
+ )
639
+
640
+ trace_accepted = gr.Checkbox(
641
+ label="Must have accepted answer",
642
+ value=True
643
+ )
644
+
645
+ trace_limit = gr.Slider(
646
+ label="Number of Results",
647
+ minimum=1,
648
+ maximum=10,
649
+ value=3,
650
+ step=1
651
+ )
652
+
653
+ trace_format = gr.Dropdown(
654
+ label="Response Format",
655
+ choices=["markdown", "json"],
656
+ value="markdown"
657
+ )
658
+
659
+ trace_btn = gr.Button("📊 Analyze Stack Trace", variant="primary", size="lg")
660
+ trace_output = gr.Markdown(label="Stack Trace Analysis", height=400)
661
+
662
+ trace_btn.click(
663
+ fn=analyze_stack_trace_sync,
664
+ inputs=[stack_trace_input, trace_language, trace_min_score, trace_accepted, trace_limit, trace_format, api_key_input],
665
+ outputs=trace_output
666
+ )
667
+
668
+ # Tab 5: Advanced Search
669
+ with gr.Tab("⚙️ Advanced Search", id="advanced"):
670
+ gr.Markdown("### Advanced search with comprehensive filtering options")
671
+
672
+ with gr.Row():
673
+ with gr.Column():
674
+ adv_query_input = gr.Textbox(
675
+ label="Search Query (optional)",
676
+ placeholder="e.g., 'memory management'",
677
+ value=""
678
+ )
679
+
680
+ adv_title_input = gr.Textbox(
681
+ label="Title Contains (optional)",
682
+ placeholder="Text that must appear in the title",
683
+ value=""
684
+ )
685
+
686
+ adv_body_input = gr.Textbox(
687
+ label="Body Contains (optional)",
688
+ placeholder="Text that must appear in the body",
689
+ value=""
690
+ )
691
+
692
+ with gr.Column():
693
+ adv_tags_input = gr.Textbox(
694
+ label="Include Tags (comma-separated)",
695
+ placeholder="e.g., python,django,performance",
696
+ value=""
697
+ )
698
+
699
+ adv_excluded_tags_input = gr.Textbox(
700
+ label="Exclude Tags (comma-separated)",
701
+ placeholder="e.g., beginner,homework",
702
+ value=""
703
+ )
704
+
705
+ adv_sort_input = gr.Dropdown(
706
+ label="Sort By",
707
+ choices=["votes", "activity", "creation", "relevance"],
708
+ value="votes"
709
+ )
710
+
711
+ with gr.Row():
712
+ adv_min_score = gr.Slider(
713
+ label="Minimum Score",
714
+ minimum=0,
715
+ maximum=500,
716
+ value=10,
717
+ step=5
718
+ )
719
+
720
+ adv_min_answers = gr.Slider(
721
+ label="Minimum Answers",
722
+ minimum=0,
723
+ maximum=50,
724
+ value=1,
725
+ step=1
726
+ )
727
+
728
+ adv_min_views = gr.Slider(
729
+ label="Minimum Views",
730
+ minimum=0,
731
+ maximum=10000,
732
+ value=0,
733
+ step=100
734
+ )
735
+
736
+ with gr.Row():
737
+ adv_accepted = gr.Checkbox(
738
+ label="Must have accepted answer",
739
+ value=False
740
+ )
741
+
742
+ adv_limit = gr.Slider(
743
+ label="Number of Results",
744
+ minimum=1,
745
+ maximum=20,
746
+ value=5,
747
+ step=1
748
+ )
749
+
750
+ adv_format = gr.Dropdown(
751
+ label="Response Format",
752
+ choices=["markdown", "json"],
753
+ value="markdown"
754
+ )
755
+
756
+ adv_search_btn = gr.Button("⚙️ Advanced Search", variant="primary", size="lg")
757
+ adv_output = gr.Markdown(label="Advanced Search Results", height=400)
758
+
759
+ adv_search_btn.click(
760
+ fn=advanced_search_sync,
761
+ inputs=[
762
+ adv_query_input, adv_tags_input, adv_excluded_tags_input,
763
+ adv_min_score, adv_title_input, adv_body_input, adv_min_answers,
764
+ adv_accepted, adv_min_views, adv_sort_input, adv_limit, adv_format, api_key_input
765
+ ],
766
+ outputs=adv_output
767
+ )
768
+
769
+ # Footer with MCP information
770
+ gr.Markdown("""
771
+ ---
772
+
773
+ ## 🤖 MCP Integration
774
+
775
+ This app also functions as an **MCP (Model Context Protocol) Server**!
776
+
777
+ To use with AI assistants like Claude Desktop, add this configuration:
778
+
779
+ ```json
780
+ {
781
+ "mcpServers": {
782
+ "stackoverflow": {
783
+ "url": "YOUR_DEPLOYED_URL/gradio_api/mcp/sse"
784
+ }
785
+ }
786
+ }
787
+ ```
788
+
789
+ **Available MCP Tools:**
790
+ - `search_by_query_sync` - General Stack Overflow search
791
+ - `search_by_error_sync` - Error-specific search
792
+ - `get_question_sync` - Get specific question by ID
793
+ - `analyze_stack_trace_sync` - Analyze stack traces
794
+ - `advanced_search_sync` - Advanced search with comprehensive filters
795
+
796
+ **💡 Pro Tip:** Add your Stack Exchange API key above for higher quotas (10,000 vs 300 requests/day)!
797
+
798
+ Built with ❤️ for the MCP Hackathon
799
+ """)
800
+
801
+
802
+ if __name__ == "__main__":
803
+ # Launch with MCP server enabled
804
+ demo.launch(
805
+ mcp_server=True,
806
+ share=True, # Create a public link for testing
807
+ server_name="0.0.0.0", # Allow external connections
808
+ server_port=7860, # Standard Gradio port
809
+ show_error=True
810
+ )
pyproject.toml ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["hatchling>=1.0.0"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "stackoverflow-mcp"
7
+ version = "0.1.3"
8
+ description = "Stack Overflow MCP server for LLM applications"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICEN[CS]E*"]
13
+ authors = [
14
+ {name = "Mark Nawar", email = "[email protected]"},
15
+ ]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Operating System :: OS Independent",
22
+ ]
23
+ dependencies = [
24
+ "httpx>=0.24.0",
25
+ "python-dotenv>=1.0.0",
26
+ "mcp>=0.7.0",
27
+ "gradio[mcp]>=5.33.1",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ dev = [
32
+ "pytest>=7.0.0",
33
+ "pytest-asyncio>=0.21.0",
34
+ "pytest-cov>=4.0.0",
35
+ ]
36
+
37
+ [project.urls]
38
+ Homepage = "https://github.com/CodexVeritax/stackoverflow-mcp-server"
39
+ Issues = "https://github.com/CodexVeritax/stackoverflow-mcp-server/issues"
40
+
41
+ [tool.hatch.build.targets.wheel]
42
+ packages = ["stackoverflow_mcp"]
43
+
44
+ [tool.hatch.build.targets.sdist]
45
+ include = [
46
+ "stackoverflow_mcp",
47
+ "LICENSE",
48
+ "README.md",
49
+ "pyproject.toml",
50
+ ]
51
+
52
+ [project.scripts]
53
+ stackoverflow-mcp = "stackoverflow_mcp.__main__:main"
54
+
55
+ [tool.mypy]
56
+ python_version = "3.8"
57
+ warn_return_any = true
58
+ warn_unused_configs = true
59
+ disallow_untyped_defs = true
60
+ disallow_incomplete_defs = true
61
+
62
+ [tool.pytest.ini_options]
63
+ testpaths = ["tests"]
64
+ python_files = "test_*.py"
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ httpx>=0.24.0
2
+ python-dotenv>=1.0.0
3
+ mcp>=0.7.0
4
+ gradio[mcp]>=5.33.1
stackoverflow_mcp/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ """Veritax Stackoverflow MCP Server.
2
+
3
+ A Model Context Protocol (MCP) server for accessing Stackoverflow question.
4
+ """
5
+
6
+ __version__ = "0.1.3"
stackoverflow_mcp/__main__.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ from stackoverflow_mcp.server import mcp
3
+
4
+ def main():
5
+ """Entry point for the Stack Overflow MCP server."""
6
+ print("Starting Stack Overflow MCP Server...")
7
+ mcp.run()
8
+
9
+ if __name__ == "__main__":
10
+ main()
stackoverflow_mcp/api.py ADDED
@@ -0,0 +1,477 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import time
3
+ from typing import Dict, List, Optional, Any, Union
4
+ import json
5
+ from dataclasses import asdict
6
+ import asyncio
7
+ from datetime import datetime
8
+ from itertools import islice
9
+
10
+ from .types import (
11
+ StackOverflowQuestion,
12
+ StackOverflowAnswer,
13
+ StackOverflowComment,
14
+ SearchResult,
15
+ SearchResultComments
16
+ )
17
+
18
+ from .env import (
19
+ MAX_REQUEST_PER_WINDOW,
20
+ RATE_LIMIT_WINDOW_MS,
21
+ RETRY_AFTER_MS
22
+ )
23
+
24
+ STACKOVERFLOW_API = "https://api.stackexchange.com/2.3"
25
+ BATCH_SIZE = 100 # API limit for batch requests
26
+
27
+ class StackExchangeAPI:
28
+ def __init__(self, api_key: Optional[str] = None):
29
+ self.api_key = api_key
30
+ self.request_timestamps = []
31
+ self.client = httpx.AsyncClient(timeout=30.0)
32
+
33
+ async def close(self):
34
+ await self.client.aclose()
35
+
36
+ def _check_rate_limit(self) -> bool:
37
+ now = time.time() * 1000
38
+
39
+ self.request_timestamps = [
40
+ ts for ts in self.request_timestamps
41
+ if now - ts < RATE_LIMIT_WINDOW_MS
42
+ ]
43
+
44
+ if len(self.request_timestamps) >= MAX_REQUEST_PER_WINDOW:
45
+ return False
46
+
47
+ self.request_timestamps.append(now)
48
+ return True
49
+
50
+ async def _with_rate_limit(self, func, *args, retries=3, attempts=10, **kwargs):
51
+ """Execute a function with rate limiting.
52
+
53
+ Args:
54
+ func (_type_): Function to execute with rate limiting
55
+ retries (int, optional): Number of retries after API rate limit error. Defaults to 3.
56
+ attempts (int, optional): Number of times to retry after hitting local rate limit. Defaults to 10.
57
+
58
+ Raises:
59
+ Exception: When maximum rate limiting attempts are exceeded
60
+ e: Original error if retries are exhausted
61
+
62
+ Returns:
63
+ Any: Result from the function
64
+ """
65
+ if retries is None:
66
+ retries = 3
67
+
68
+ if attempts <= 0:
69
+ raise Exception("Maximum rate limiting attempts exceeded")
70
+
71
+ if not self._check_rate_limit():
72
+ print("Rate limit exceeded, waiting before retry")
73
+ await asyncio.sleep(RETRY_AFTER_MS / 1000)
74
+ return await self._with_rate_limit(func, *args, retries=retries, attempts=attempts-1, **kwargs)
75
+
76
+ try:
77
+ return await func(*args, **kwargs)
78
+ except httpx.HTTPStatusError as e:
79
+ if retries > 0 and e.response.status_code == 429:
80
+ print("Rate limit hit (429), retrying after delay...")
81
+ await asyncio.sleep(RETRY_AFTER_MS/1000)
82
+ return await self._with_rate_limit(func, *args, retries=retries-1, attempts=attempts, **kwargs)
83
+ raise e
84
+
85
+ async def fetch_batch_answers(self, question_ids: List[int]) -> Dict[int, List[StackOverflowAnswer]]:
86
+ """Fetch answers for multiple questions in a single API call.
87
+
88
+ Args:
89
+ question_ids (List[int]): List of Stack Overflow question IDs
90
+
91
+ Returns:
92
+ Dict[int, List[StackOverflowAnswer]]: Dictionary mapping question IDs to their answers
93
+ """
94
+ if not question_ids:
95
+ return {}
96
+
97
+ result = {}
98
+
99
+ # Process in batches of BATCH_SIZE (API limit)
100
+ for i in range(0, len(question_ids), BATCH_SIZE):
101
+ batch = question_ids[i:i+BATCH_SIZE]
102
+ ids_string = ";".join(str(qid) for qid in batch)
103
+
104
+ params = {
105
+ "site": "stackoverflow",
106
+ "sort": "votes",
107
+ "order": "desc",
108
+ "filter": "withbody",
109
+ "pagesize": "100"
110
+ }
111
+
112
+ if self.api_key:
113
+ params["key"] = self.api_key
114
+
115
+ async def _do_fetch():
116
+ response = await self.client.get(
117
+ f"{STACKOVERFLOW_API}/questions/{ids_string}/answers",
118
+ params=params
119
+ )
120
+ response.raise_for_status()
121
+ return response.json()
122
+
123
+ data = await self._with_rate_limit(_do_fetch)
124
+
125
+ for answer_data in data.get("items", []):
126
+ question_id = answer_data.get("question_id")
127
+ if question_id not in result:
128
+ result[question_id] = []
129
+
130
+ answer = StackOverflowAnswer(
131
+ answer_id=answer_data.get("answer_id"),
132
+ question_id=question_id,
133
+ score=answer_data.get("score", 0),
134
+ is_accepted=answer_data.get("is_accepted", False),
135
+ body=answer_data.get("body", ""),
136
+ creation_date=answer_data.get("creation_date", 0),
137
+ last_activity_date=answer_data.get("last_activity_date", 0),
138
+ link=answer_data.get("link", ""),
139
+ owner=answer_data.get("owner")
140
+ )
141
+ result[question_id].append(answer)
142
+
143
+ return result
144
+
145
+ async def fetch_batch_comments(self, post_ids: List[int]) -> Dict[int, List[StackOverflowComment]]:
146
+ """Fetch comments for multiple posts in a single API call.
147
+
148
+ Args:
149
+ post_ids (List[int]): List of Stack Overflow post IDs (questions or answers)
150
+
151
+ Returns:
152
+ Dict[int, List[StackOverflowComment]]: Dictionary mapping post IDs to their comments
153
+ """
154
+ if not post_ids:
155
+ return {}
156
+
157
+ result = {}
158
+
159
+ # Process in batches of BATCH_SIZE (API limit)
160
+ for i in range(0, len(post_ids), BATCH_SIZE):
161
+ batch = post_ids[i:i+BATCH_SIZE]
162
+ ids_string = ";".join(str(pid) for pid in batch)
163
+
164
+ params = {
165
+ "site": "stackoverflow",
166
+ "sort": "votes",
167
+ "order": "desc",
168
+ "filter": "withbody",
169
+ "pagesize": "100"
170
+ }
171
+
172
+ if self.api_key:
173
+ params["key"] = self.api_key
174
+
175
+ async def _do_fetch():
176
+ response = await self.client.get(
177
+ f"{STACKOVERFLOW_API}/posts/{ids_string}/comments",
178
+ params=params
179
+ )
180
+ response.raise_for_status()
181
+ return response.json()
182
+
183
+ data = await self._with_rate_limit(_do_fetch)
184
+
185
+ for comment_data in data.get("items", []):
186
+ post_id = comment_data.get("post_id")
187
+ if post_id not in result:
188
+ result[post_id] = []
189
+
190
+ comment = StackOverflowComment(
191
+ comment_id=comment_data.get("comment_id"),
192
+ post_id=post_id,
193
+ score=comment_data.get("score", 0),
194
+ body=comment_data.get("body", ""),
195
+ creation_date=comment_data.get("creation_date", 0),
196
+ owner=comment_data.get("owner")
197
+ )
198
+ result[post_id].append(comment)
199
+
200
+ return result
201
+
202
+ async def advanced_search(
203
+ self,
204
+ query: Optional[str] = None,
205
+ tags: Optional[List[str]] = None,
206
+ excluded_tags: Optional[List[str]] = None,
207
+ min_score: Optional[int] = None,
208
+ title: Optional[str] = None,
209
+ body: Optional[str] = None,
210
+ answers: Optional[int] = None,
211
+ has_accepted_answer: Optional[bool] = None,
212
+ views: Optional[int] = None,
213
+ url: Optional[str] = None,
214
+ user_id: Optional[int] = None,
215
+ is_closed: Optional[bool] = None,
216
+ is_wiki: Optional[bool] = None,
217
+ is_migrated: Optional[bool] = None,
218
+ has_notice: Optional[bool] = None,
219
+ from_date: Optional[datetime] = None,
220
+ to_date: Optional[datetime] = None,
221
+ sort_by: Optional[str] = "votes",
222
+ limit: Optional[int] = 5,
223
+ include_comments: bool = False,
224
+ retries: Optional[int] = 3
225
+ ) -> List[SearchResult]:
226
+ """Advanced search for Stack Overflow questions with many filter options."""
227
+ params = {
228
+ "site": "stackoverflow",
229
+ "sort": sort_by,
230
+ "order": "desc",
231
+ "filter": "withbody"
232
+ }
233
+
234
+ if query:
235
+ params["q"] = query
236
+
237
+ if tags:
238
+ params["tagged"] = ";".join(tags)
239
+
240
+ if excluded_tags:
241
+ params["nottagged"] = ";".join(excluded_tags)
242
+
243
+ if title:
244
+ params["title"] = title
245
+
246
+ if body:
247
+ params["body"] = body
248
+
249
+ if answers is not None:
250
+ params["answers"] = str(answers)
251
+
252
+ if has_accepted_answer is not None:
253
+ params["accepted"] = "true" if has_accepted_answer else "false"
254
+
255
+ if views is not None:
256
+ params["views"] = str(views)
257
+
258
+ if url:
259
+ params["url"] = url
260
+
261
+ if user_id is not None:
262
+ params["user"] = str(user_id)
263
+
264
+ if is_closed is not None:
265
+ params["closed"] = "true" if is_closed else "false"
266
+
267
+ if is_wiki is not None:
268
+ params["wiki"] = "true" if is_wiki else "false"
269
+
270
+ if is_migrated is not None:
271
+ params["migrated"] = "true" if is_migrated else "false"
272
+
273
+ if has_notice is not None:
274
+ params["notice"] = "true" if has_notice else "false"
275
+
276
+ if from_date:
277
+ params["fromdate"] = str(int(from_date.timestamp()))
278
+
279
+ if to_date:
280
+ params["todate"] = str(int(to_date.timestamp()))
281
+
282
+ if limit:
283
+ params["pagesize"] = str(limit)
284
+
285
+ if self.api_key:
286
+ params["key"] = self.api_key
287
+
288
+ async def _do_search():
289
+ response = await self.client.get(f"{STACKOVERFLOW_API}/search/advanced", params=params)
290
+ response.raise_for_status()
291
+ return response.json()
292
+
293
+ data = await self._with_rate_limit(_do_search, retries=retries)
294
+
295
+ questions = []
296
+ question_ids = []
297
+
298
+ for question_data in data.get("items", []):
299
+ if min_score is not None and question_data.get("score", 0) < min_score:
300
+ continue
301
+
302
+ question = StackOverflowQuestion(
303
+ question_id=question_data.get("question_id"),
304
+ title=question_data.get("title", ""),
305
+ body=question_data.get("body", ""),
306
+ score=question_data.get("score", 0),
307
+ answer_count=question_data.get("answer_count", 0),
308
+ is_answered=question_data.get("is_answered", False),
309
+ accepted_answer_id=question_data.get("accepted_answer_id"),
310
+ creation_date=question_data.get("creation_date", 0),
311
+ last_activity_date=question_data.get("last_activity_date", 0),
312
+ view_count=question_data.get("view_count", 0),
313
+ tags=question_data.get("tags", []),
314
+ link=question_data.get("link", ""),
315
+ is_closed=question_data.get("closed_date") is not None,
316
+ owner=question_data.get("owner")
317
+ )
318
+ questions.append(question)
319
+ question_ids.append(question.question_id)
320
+
321
+ answers_by_question = await self.fetch_batch_answers(question_ids)
322
+
323
+ results = []
324
+
325
+ if include_comments:
326
+ all_post_ids = question_ids.copy()
327
+ for qid, answers in answers_by_question.items():
328
+ all_post_ids.extend([a.answer_id for a in answers])
329
+
330
+ # Batch fetch all comments
331
+ all_comments = await self.fetch_batch_comments(all_post_ids)
332
+
333
+ # Construct results with comments
334
+ for question in questions:
335
+ question_answers = answers_by_question.get(question.question_id, [])
336
+
337
+ # Create comment structure
338
+ question_comments = all_comments.get(question.question_id, [])
339
+ answer_comments = {}
340
+
341
+ for answer in question_answers:
342
+ answer_comments[answer.answer_id] = all_comments.get(answer.answer_id, [])
343
+
344
+ comments = SearchResultComments(
345
+ question=question_comments,
346
+ answers=answer_comments
347
+ )
348
+
349
+ results.append(SearchResult(
350
+ question=question,
351
+ answers=question_answers,
352
+ comments=comments
353
+ ))
354
+ else:
355
+ for question in questions:
356
+ question_answers = answers_by_question.get(question.question_id, [])
357
+ results.append(SearchResult(
358
+ question=question,
359
+ answers=question_answers,
360
+ comments=None
361
+ ))
362
+
363
+ return results
364
+
365
+ async def search_by_query(
366
+ self,
367
+ query: str,
368
+ tags: Optional[List[str]] = None,
369
+ excluded_tags: Optional[List[str]] = None,
370
+ min_score: Optional[int] = None,
371
+ title: Optional[str] = None,
372
+ body: Optional[str] = None,
373
+ has_accepted_answer: Optional[bool] = None,
374
+ answers: Optional[int] = None,
375
+ sort_by: Optional[str] = "votes",
376
+ limit: Optional[int] = 5,
377
+ include_comments: bool = False,
378
+ retries: Optional[int] = 3
379
+ ) -> List[SearchResult]:
380
+ """Search Stack Overflow for questions matching a query with additional filters."""
381
+ return await self.advanced_search(
382
+ query=query,
383
+ tags=tags,
384
+ excluded_tags=excluded_tags,
385
+ min_score=min_score,
386
+ title=title,
387
+ body=body,
388
+ has_accepted_answer=has_accepted_answer,
389
+ answers=answers,
390
+ sort_by=sort_by,
391
+ limit=limit,
392
+ include_comments=include_comments,
393
+ retries=retries
394
+ )
395
+
396
+ async def fetch_answers(self, question_id: int) -> List[StackOverflowAnswer]:
397
+ """Fetch answers for a specific question.
398
+
399
+ Note: This is kept for backward compatibility, but new code should
400
+ use fetch_batch_answers for better performance.
401
+ """
402
+ answers_dict = await self.fetch_batch_answers([question_id])
403
+ return answers_dict.get(question_id, [])
404
+
405
+ async def fetch_comments(self, post_id: int) -> List[StackOverflowComment]:
406
+ """Fetch comments for a specific post.
407
+
408
+ Note: This is kept for backward compatibility, but new code should
409
+ use fetch_batch_comments for better performance.
410
+ """
411
+ comments_dict = await self.fetch_batch_comments([post_id])
412
+ return comments_dict.get(post_id, [])
413
+
414
+ async def get_question(self, question_id: int, include_comments: bool = True) -> SearchResult:
415
+ """Get a specific question by ID."""
416
+ params = {
417
+ "site": "stackoverflow",
418
+ "filter": "withbody"
419
+ }
420
+
421
+ if self.api_key:
422
+ params["key"] = self.api_key
423
+
424
+ async def _do_fetch():
425
+ response = await self.client.get(
426
+ f"{STACKOVERFLOW_API}/questions/{question_id}",
427
+ params=params
428
+ )
429
+ response.raise_for_status()
430
+ return response.json()
431
+
432
+ data = await self._with_rate_limit(_do_fetch)
433
+
434
+ if not data.get("items"):
435
+ raise ValueError(f"Question with ID {question_id} not found")
436
+
437
+ question_data = data["items"][0]
438
+ question = StackOverflowQuestion(
439
+ question_id=question_data.get("question_id"),
440
+ title=question_data.get("title", ""),
441
+ body=question_data.get("body", ""),
442
+ score=question_data.get("score", 0),
443
+ answer_count=question_data.get("answer_count", 0),
444
+ is_answered=question_data.get("is_answered", False),
445
+ accepted_answer_id=question_data.get("accepted_answer_id"),
446
+ creation_date=question_data.get("creation_date", 0),
447
+ last_activity_date=question_data.get("last_activity_date", 0),
448
+ view_count=question_data.get("view_count", 0),
449
+ tags=question_data.get("tags", []),
450
+ link=question_data.get("link", ""),
451
+ is_closed=question_data.get("closed_date") is not None,
452
+ owner=question_data.get("owner")
453
+ )
454
+
455
+ answers = await self.fetch_answers(question.question_id)
456
+
457
+ comments = None
458
+ if include_comments:
459
+ post_ids = [question.question_id] + [answer.answer_id for answer in answers]
460
+ all_comments = await self.fetch_batch_comments(post_ids)
461
+
462
+ question_comments = all_comments.get(question.question_id, [])
463
+ answer_comments = {}
464
+
465
+ for answer in answers:
466
+ answer_comments[answer.answer_id] = all_comments.get(answer.answer_id, [])
467
+
468
+ comments = SearchResultComments(
469
+ question=question_comments,
470
+ answers=answer_comments
471
+ )
472
+
473
+ return SearchResult(
474
+ question=question,
475
+ answers=answers,
476
+ comments=comments
477
+ )
stackoverflow_mcp/env.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ load_dotenv()
5
+
6
+ STACK_EXCHANGE_API_KEY = os.getenv("STACK_EXCHANGE_API_KEY")
7
+
8
+ MAX_REQUEST_PER_WINDOW = int(os.getenv("MAX_REQUEST_PER_WINDOW" , "30"))
9
+ RATE_LIMIT_WINDOW_MS = int(os.getenv("RATE_LIMIT_WINDOW_MS" , "60000"))
10
+ RETRY_AFTER_MS = int(os.getenv("RETRY_AFTER_MS" , "2000"))
stackoverflow_mcp/formatter.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from typing import List
3
+ from dataclasses import asdict
4
+ import re
5
+
6
+ from .types import SearchResult, StackOverflowAnswer, StackOverflowComment
7
+
8
+
9
+ def format_response(results: List[SearchResult], format_type: str = "markdown") -> str:
10
+ """Format search results as either JSON or Markdown.
11
+
12
+ Args:
13
+ results (List[SearchResult]): List of search results to format
14
+ format_type (str, optional): Output format type - either "json" or "markdown". Defaults to "markdown".
15
+
16
+ Returns:
17
+ str: Formatted string representation of the search results
18
+ """
19
+
20
+ if format_type == "json":
21
+ def _convert_to_dict(obj):
22
+ if hasattr(obj, "__dataclass_fields__"):
23
+ return asdict(obj)
24
+ return obj
25
+
26
+ class DataClassJSONEncoder(json.JSONEncoder):
27
+ def default(self, obj):
28
+ if hasattr(obj, "__dataclass_fields__"):
29
+ return asdict(obj)
30
+ return super().default(obj)
31
+
32
+ return json.dumps(results, cls=DataClassJSONEncoder, indent=2)
33
+
34
+ if not results:
35
+ return "No results found."
36
+
37
+ markdown = ""
38
+
39
+ for result in results:
40
+ markdown += f"# {result.question.title}\n\n"
41
+ markdown += f"**Score:** {result.question.score} | **Answers:** {result.question.answer_count}\n\n"
42
+
43
+ question_body = clean_html(result.question.body)
44
+ markdown += f"## Question\n\n{question_body}\n\n"
45
+
46
+ if result.comments and result.comments.question:
47
+ markdown += "### Question Comments\n\n"
48
+ for comment in result.comments.question:
49
+ markdown += f"- {clean_html(comment.body)} *(Score: {comment.score})*\n"
50
+ markdown += "\n"
51
+
52
+ markdown += "## Answers\n\n"
53
+ for answer in result.answers:
54
+ markdown += f"### {'✓ ' if answer.is_accepted else ''}Answer (Score: {answer.score})\n\n"
55
+ answer_body = clean_html(answer.body)
56
+ markdown += f"{answer_body}\n\n"
57
+
58
+ if (result.comments and
59
+ result.comments.answers and
60
+ answer.answer_id in result.comments.answers and
61
+ result.comments.answers[answer.answer_id]
62
+ ):
63
+ markdown += "#### Answer Comments\n\n"
64
+ for comment in result.comments.answers[answer.answer_id]:
65
+ markdown += f"- {clean_html(comment.body)} *(Score: {comment.score})*\n"
66
+
67
+ markdown += "/n"
68
+
69
+ markdown += f"---\n\n[View on Stack Overflow]({result.question.link})\n\n"
70
+
71
+ return markdown
72
+
73
+ def clean_html(html_text: str) -> str:
74
+ """Clean HTML tags from text while preserving code blocks.
75
+
76
+ Args:
77
+ html_text (str): HTML text to be cleaned
78
+
79
+ Returns:
80
+ str: Cleaned text with HTML tags removed and code blocks preserved
81
+ """
82
+
83
+ code_blocks = []
84
+
85
+ def replace_code_block(match):
86
+ code = match.group(1) or match.group(2)
87
+ code_blocks.append(code)
88
+ return f"CODE_BLOCK_{len(code_blocks)-1}"
89
+
90
+ html_without_code = re.sub(r'<pre><code>(.*?)</code></pre>|<code>(.*?)</code>', replace_code_block, html_text, flags=re.DOTALL)
91
+
92
+ text_without_html = re.sub(r'<[^>]+>', '', html_without_code)
93
+
94
+ for i, code in enumerate(code_blocks):
95
+ if '\n' in code or len(code) > 80:
96
+ text_without_html = text_without_html.replace(f"CODE_BLOCK_{i}", f"```\n{code}\n```")
97
+ else:
98
+ text_without_html = text_without_html.replace(f"CODE_BLOCK_{i}", f"`{code}`")
99
+
100
+
101
+ text_without_html = text_without_html.replace("&lt;", "<")
102
+ text_without_html = text_without_html.replace("&gt;", ">")
103
+ text_without_html = text_without_html.replace("&amp;", "&")
104
+ text_without_html = text_without_html.replace("&quot;", "\"")
105
+
106
+ return text_without_html
stackoverflow_mcp/server.py ADDED
@@ -0,0 +1,376 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ from contextlib import asynccontextmanager
3
+ from dataclasses import dataclass
4
+ from typing import AsyncIterator, List, Optional, Dict, Any
5
+ from datetime import datetime
6
+
7
+ from mcp.server.fastmcp import FastMCP, Context
8
+ # Instead of importing Error from mcp.server.fastmcp.tools, we'll define our own Error class
9
+ # or we can use standard exceptions for now
10
+
11
+ from .api import StackExchangeAPI
12
+ from .types import (
13
+ SearchByQueryInput,
14
+ SearchByErrorInput,
15
+ GetQuestionInput,
16
+ AdvancedSearchInput,
17
+ SearchResult
18
+ )
19
+
20
+ from .formatter import format_response
21
+ from .env import STACK_EXCHANGE_API_KEY
22
+
23
+ @dataclass
24
+ class AppContext:
25
+ api: StackExchangeAPI
26
+
27
+ @asynccontextmanager
28
+ async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
29
+ """Manage application lifecycle with the Stack Exchange API client.
30
+
31
+ Args:
32
+ server (FastMCP): The FastMCP server instance
33
+
34
+ Returns:
35
+ AsyncIterator[AppContext]: Context containing the API client
36
+ """
37
+
38
+ api = StackExchangeAPI(
39
+ api_key=STACK_EXCHANGE_API_KEY,
40
+ )
41
+ try:
42
+ yield AppContext(api=api)
43
+ finally:
44
+ await api.close()
45
+
46
+ mcp = FastMCP(
47
+ "Stack Overflow MCP",
48
+ lifespan=app_lifespan,
49
+ dependencies=["httpx", "python-dotenv"]
50
+ )
51
+
52
+ @mcp.tool()
53
+ async def advanced_search(
54
+ query: Optional[str] = None,
55
+ tags: Optional[List[str]] = None,
56
+ excluded_tags: Optional[List[str]] = None,
57
+ min_score: Optional[int] = None,
58
+ title: Optional[str] = None,
59
+ body: Optional[str] = None,
60
+ answers: Optional[int] = None,
61
+ has_accepted_answer: Optional[bool] = None,
62
+ views: Optional[int] = None,
63
+ url: Optional[str] = None,
64
+ user_id: Optional[int] = None,
65
+ is_closed: Optional[bool] = None,
66
+ is_wiki: Optional[bool] = None,
67
+ is_migrated: Optional[bool] = None,
68
+ has_notice: Optional[bool] = None,
69
+ from_date: Optional[datetime] = None,
70
+ to_date: Optional[datetime] = None,
71
+ sort_by: Optional[str] = "votes",
72
+ include_comments: Optional[bool] = False,
73
+ response_format: Optional[str] = "markdown",
74
+ limit: Optional[int] = 5,
75
+ ctx: Context = None
76
+ ) -> str:
77
+ """Advanced search for Stack Overflow questions with many filter options.
78
+
79
+ Args:
80
+ query (Optional[str]): Free-form search query
81
+ tags (Optional[List[str]]): List of tags to filter by
82
+ excluded_tags (Optional[List[str]]): List of tags to exclude
83
+ min_score (Optional[int]): Minimum score threshold
84
+ title (Optional[str]): Text that must appear in the title
85
+ body (Optional[str]): Text that must appear in the body
86
+ answers (Optional[int]): Minimum number of answers
87
+ has_accepted_answer (Optional[bool]): Whether questions must have an accepted answer
88
+ views (Optional[int]): Minimum number of views
89
+ url (Optional[str]): URL that must be contained in the post
90
+ user_id (Optional[int]): ID of the user who must own the questions
91
+ is_closed (Optional[bool]): Whether to return only closed or open questions
92
+ is_wiki (Optional[bool]): Whether to return only community wiki questions
93
+ is_migrated (Optional[bool]): Whether to return only migrated questions
94
+ has_notice (Optional[bool]): Whether to return only questions with post notices
95
+ from_date (Optional[datetime]): Earliest creation date
96
+ to_date (Optional[datetime]): Latest creation date
97
+ sort_by (Optional[str]): Field to sort by (activity, creation, votes, relevance)
98
+ include_comments (Optional[bool]): Whether to include comments in results
99
+ response_format (Optional[str]): Format of response ("json" or "markdown")
100
+ limit (Optional[int]): Maximum number of results to return
101
+ ctx (Context): The context is passed automatically by the MCP
102
+
103
+ Returns:
104
+ str: Formatted search results
105
+ """
106
+ try:
107
+ api = ctx.request_context.lifespan_context.api
108
+
109
+ ctx.debug(f"Performing advanced search on Stack Overflow")
110
+ if query:
111
+ ctx.debug(f"Query: {query}")
112
+ if body:
113
+ ctx.debug(f"Body: {body}")
114
+ if tags:
115
+ ctx.debug(f"Tags: {', '.join(tags)}")
116
+ if excluded_tags:
117
+ ctx.debug(f"Excluded tags: {', '.join(excluded_tags)}")
118
+
119
+ results = await api.advanced_search(
120
+ query=query,
121
+ tags=tags,
122
+ excluded_tags=excluded_tags,
123
+ min_score=min_score,
124
+ title=title,
125
+ body=body,
126
+ answers=answers,
127
+ has_accepted_answer=has_accepted_answer,
128
+ views=views,
129
+ url=url,
130
+ user_id=user_id,
131
+ is_closed=is_closed,
132
+ is_wiki=is_wiki,
133
+ is_migrated=is_migrated,
134
+ has_notice=has_notice,
135
+ from_date=from_date,
136
+ to_date=to_date,
137
+ sort_by=sort_by,
138
+ limit=limit,
139
+ include_comments=include_comments
140
+ )
141
+
142
+ ctx.debug(f"Found {len(results)} results")
143
+
144
+ return format_response(results, response_format)
145
+
146
+ except Exception as e:
147
+ ctx.error(f"Error performing advanced search on Stack Overflow: {str(e)}")
148
+ raise RuntimeError(f"Failed to search Stack Overflow: {str(e)}")
149
+
150
+ @mcp.tool()
151
+ async def search_by_query(
152
+ query: str,
153
+ tags: Optional[List[str]] = None,
154
+ excluded_tags: Optional[List[str]] = None,
155
+ min_score: Optional[int] = None,
156
+ title: Optional[str] = None,
157
+ body: Optional[str] = None,
158
+ has_accepted_answer: Optional[bool] = None,
159
+ answers: Optional[int] = None,
160
+ sort_by: Optional[str] = "votes",
161
+ include_comments: Optional[bool] = False,
162
+ response_format: Optional[str] = "markdown",
163
+ limit: Optional[int] = 5,
164
+ ctx: Context = None
165
+ ) -> str:
166
+ """Search Stack Overflow for questions matching a query.
167
+
168
+ Args:
169
+ query (str): The search query
170
+ tags (Optional[List[str]]): Optional list of tags to filter by (e.g., ["python", "pandas"])
171
+ excluded_tags (Optional[List[str]]): Optional list of tags to exclude
172
+ min_score (Optional[int]): Minimum score threshold for questions
173
+ title (Optional[str]): Text that must appear in the title
174
+ body (Optional[str]): Text that must appear in the body
175
+ has_accepted_answer (Optional[bool]): Whether questions must have an accepted answer
176
+ answers (Optional[int]): Minimum number of answers
177
+ sort_by (Optional[str]): Field to sort by (activity, creation, votes, relevance)
178
+ include_comments (Optional[bool]): Whether to include comments in results
179
+ response_format (Optional[str]): Format of response ("json" or "markdown")
180
+ limit (Optional[int]): Maximum number of results to return
181
+ ctx (Context): The context is passed automatically by the MCP
182
+
183
+ Returns:
184
+ str: Formatted search results
185
+ """
186
+ try:
187
+ api = ctx.request_context.lifespan_context.api
188
+
189
+ ctx.debug(f"Searching Stack Overflow for: {query}")
190
+
191
+ if tags:
192
+ ctx.debug(f"Filtering by tags: {', '.join(tags)}")
193
+ if excluded_tags:
194
+ ctx.debug(f"Excluding tags: {', '.join(excluded_tags)}")
195
+
196
+ results = await api.search_by_query(
197
+ query=query,
198
+ tags=tags,
199
+ excluded_tags=excluded_tags,
200
+ min_score=min_score,
201
+ title=title,
202
+ body=body,
203
+ has_accepted_answer=has_accepted_answer,
204
+ answers=answers,
205
+ sort_by=sort_by,
206
+ limit=limit,
207
+ include_comments=include_comments
208
+ )
209
+
210
+ ctx.debug(f"Found {len(results)} results")
211
+
212
+ return format_response(results, response_format)
213
+
214
+ except Exception as e:
215
+ ctx.error(f"Error searching Stack Overflow: {str(e)}")
216
+ raise RuntimeError(f"Failed to search Stack Overflow: {str(e)}")
217
+
218
+
219
+ @mcp.tool()
220
+ async def search_by_error(
221
+ error_message: str,
222
+ language: Optional[str] = None,
223
+ technologies: Optional[List[str]] = None,
224
+ excluded_tags: Optional[List[str]] = None,
225
+ min_score: Optional[int] = None,
226
+ has_accepted_answer: Optional[bool] = None,
227
+ answers: Optional[int] = None,
228
+ include_comments: Optional[bool] = False,
229
+ response_format: Optional[str] = "markdown",
230
+ limit: Optional[int] = 5,
231
+ ctx: Context = None
232
+ ) -> str:
233
+ """Search Stack Overflow for solutions to an error message
234
+
235
+ Args:
236
+ error_message (str): The error message to search for
237
+ language (Optional[str]): Programming language (e.g., "python", "javascript")
238
+ technologies (Optional[List[str]]): Related technologies (e.g., ["react", "django"])
239
+ excluded_tags (Optional[List[str]]): Optional list of tags to exclude
240
+ min_score (Optional[int]): Minimum score threshold for questions
241
+ has_accepted_answer (Optional[bool]): Whether questions must have an accepted answer
242
+ answers (Optional[int]): Minimum number of answers
243
+ include_comments (Optional[bool]): Whether to include comments in results
244
+ response_format (Optional[str]): Format of response ("json" or "markdown")
245
+ limit (Optional[int]): Maximum number of results to return
246
+ ctx (Context): The context is passed automatically by the MCP
247
+
248
+ Returns:
249
+ str: Formatted search results
250
+ """
251
+ try:
252
+ api = ctx.request_context.lifespan_context.api
253
+
254
+ tags = []
255
+ if language:
256
+ tags.append(language.lower())
257
+ if technologies:
258
+ tags.extend([t.lower() for t in technologies])
259
+
260
+ ctx.debug(f"Searching Stack Overflow for error: {error_message}")
261
+
262
+ if tags:
263
+ ctx.debug(f"Using tags: {', '.join(tags)}")
264
+ if excluded_tags:
265
+ ctx.debug(f"Excluding tags: {', '.join(excluded_tags)}")
266
+
267
+ results = await api.search_by_query(
268
+ query=error_message,
269
+ tags=tags if tags else None,
270
+ excluded_tags=excluded_tags,
271
+ min_score=min_score,
272
+ has_accepted_answer=has_accepted_answer,
273
+ answers=answers,
274
+ limit=limit,
275
+ include_comments=include_comments
276
+ )
277
+ ctx.debug(f"Found {len(results)} results")
278
+
279
+ return format_response(results, response_format)
280
+ except Exception as e:
281
+ ctx.error(f"Error searching Stack Overflow: {str(e)}")
282
+ raise RuntimeError(f"Failed to search Stack Overflow: {str(e)}")
283
+
284
+ @mcp.tool()
285
+ async def get_question(
286
+ question_id: int,
287
+ include_comments: Optional[bool] = True,
288
+ response_format: Optional[str] = "markdown",
289
+ ctx: Context = None
290
+ ) -> str:
291
+ """Get a specific Stack Overflow question by ID.
292
+
293
+ Args:
294
+ question_id (int): The Stack Overflow question ID
295
+ include_comments (Optional[bool]): Whether to include comments in results
296
+ response_format (Optional[str]): Format of response ("json" or "markdown")
297
+ ctx (Context): The context is passed automatically by the MCP
298
+
299
+ Returns:
300
+ str: Formatted question details
301
+ """
302
+ try:
303
+ api = ctx.request_context.lifespan_context.api
304
+
305
+ ctx.debug(f"Fetching Stack Overflow question: {question_id}")
306
+
307
+ result = await api.get_question(
308
+ question_id=question_id,
309
+ include_comments=include_comments
310
+ )
311
+
312
+ return format_response([result], response_format)
313
+
314
+ except Exception as e:
315
+ ctx.error(f"Error fetching Stack Overflow question: {str(e)}")
316
+ raise RuntimeError(f"Failed to fetch Stack Overflow question: {str(e)}")
317
+
318
+ @mcp.tool()
319
+ async def analyze_stack_trace(
320
+ stack_trace: str,
321
+ language: str,
322
+ excluded_tags: Optional[List[str]] = None,
323
+ min_score: Optional[int] = None,
324
+ has_accepted_answer: Optional[bool] = None,
325
+ answers: Optional[int] = None,
326
+ include_comments: Optional[bool] = True,
327
+ response_format: Optional[str] = "markdown",
328
+ limit: Optional[int] = 3,
329
+ ctx: Context = None
330
+ ) -> str:
331
+ """Analyze a stack trace and find relevant solutions on Stack Overflow.
332
+
333
+ Args:
334
+ stack_trace (str): The stack trace to analyze
335
+ language (str): Programming language of the stack trace
336
+ excluded_tags (Optional[List[str]]): Optional list of tags to exclude
337
+ min_score (Optional[int]): Minimum score threshold for questions
338
+ has_accepted_answer (Optional[bool]): Whether questions must have an accepted answer
339
+ answers (Optional[int]): Minimum number of answers
340
+ include_comments (Optional[bool]): Whether to include comments in results
341
+ response_format (Optional[str]): Format of response ("json" or "markdown")
342
+ limit (Optional[int]): Maximum number of results to return
343
+ ctx (Context): The context is passed automatically by the MCP
344
+
345
+ Returns:
346
+ str: Formatted search results
347
+ """
348
+ try:
349
+ api = ctx.request_context.lifespan_context.api
350
+
351
+ error_lines = stack_trace.split("\n")
352
+ error_message = error_lines[0]
353
+
354
+ ctx.debug(f"Analyzing stack trace: {error_message}")
355
+ ctx.debug(f"Language: {language}")
356
+
357
+ results = await api.search_by_query(
358
+ query=error_message,
359
+ tags=[language.lower()],
360
+ excluded_tags=excluded_tags,
361
+ min_score=min_score,
362
+ has_accepted_answer=has_accepted_answer,
363
+ answers=answers,
364
+ limit=limit,
365
+ include_comments=include_comments
366
+ )
367
+
368
+ ctx.debug(f"Found {len(results)} results")
369
+
370
+ return format_response(results, response_format)
371
+ except Exception as e:
372
+ ctx.error(f"Error analyzing stack trace: {str(e)}")
373
+ raise RuntimeError(f"Failed to analyze stack trace: {str(e)}")
374
+
375
+ if __name__ == "__main__":
376
+ mcp.run()
stackoverflow_mcp/types.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import List, Dict, Optional, Union, Literal
3
+ from datetime import datetime
4
+
5
+ @dataclass
6
+ class AdvancedSearchInput:
7
+ query: Optional[str] = None
8
+ tags: Optional[List[str]] = None
9
+ excluded_tags: Optional[List[str]] = None
10
+ min_score: Optional[int] = None
11
+ title: Optional[str] = None
12
+ body: Optional[str] = None
13
+ answers: Optional[int] = None
14
+ has_accepted_answer: Optional[bool] = None
15
+ views: Optional[int] = None
16
+ url: Optional[str] = None
17
+ user_id: Optional[int] = None
18
+ is_closed: Optional[bool] = None
19
+ is_wiki: Optional[bool] = None
20
+ is_migrated: Optional[bool] = None
21
+ has_notice: Optional[bool] = None
22
+ from_date: Optional[datetime] = None
23
+ to_date: Optional[datetime] = None
24
+ sort_by: Optional[Literal["activity", "creation", "votes", "relevance"]] = "activity"
25
+ include_comments: Optional[bool] = False
26
+ response_format: Optional[Literal["json", "markdown"]] = "markdown"
27
+ limit: Optional[int] = 5
28
+
29
+ @dataclass
30
+ class SearchByQueryInput:
31
+ query: str
32
+ tags: Optional[List[str]] = None
33
+ excluded_tags: Optional[List[str]] = None
34
+ min_score: Optional[int] = None
35
+ title: Optional[str] = None
36
+ body: Optional[str] = None
37
+ has_accepted_answer: Optional[bool] = None
38
+ answers: Optional[int] = None
39
+ sort_by: Optional[Literal["activity", "creation", "votes", "relevance"]] = "votes"
40
+ include_comments: Optional[bool] = False
41
+ response_format: Optional[Literal["json","markdown"]] = "markdown"
42
+ limit: Optional[int] = 5
43
+
44
+ @dataclass
45
+ class SearchByErrorInput:
46
+ error_message: str
47
+ language: Optional[str] = None
48
+ technologies: Optional[List[str]] = None
49
+ excluded_tags: Optional[List[str]] = None
50
+ min_score: Optional[int] = None
51
+ has_accepted_answer: Optional[bool] = None
52
+ answers: Optional[int] = None
53
+ include_comments: Optional[bool] = False
54
+ response_format: Optional[Literal["json", "markdown"]] = "markdown"
55
+ limit: Optional[int] = 5
56
+
57
+ @dataclass
58
+ class GetQuestionInput:
59
+ question_id: int
60
+ include_comments: Optional[bool] = True
61
+ response_format: Optional[Literal["json", "markdown"]] = "markdown"
62
+
63
+ @dataclass
64
+ class StackOverflowQuestion:
65
+ question_id: int
66
+ title: str
67
+ body: str
68
+ score: int
69
+ answer_count: int
70
+ is_answered: bool
71
+ accepted_answer_id: Optional[int] = None
72
+ creation_date: int = 0
73
+ last_activity_date: int = 0
74
+ view_count: int = 0
75
+ tags: List[str] = None
76
+ link: str = ""
77
+ is_closed: bool = False
78
+ owner: Optional[Dict] = None
79
+
80
+ @dataclass
81
+ class StackOverflowAnswer:
82
+ answer_id: int
83
+ question_id: int
84
+ score: int
85
+ is_accepted: bool
86
+ body: str
87
+ creation_date: int = 0
88
+ last_activity_date: int = 0
89
+ link: str = ""
90
+ owner: Optional[Dict] = None
91
+
92
+ @dataclass
93
+ class StackOverflowComment:
94
+ comment_id: int
95
+ post_id: int
96
+ score: int
97
+ body: str
98
+ creation_date: int = 0
99
+ owner: Optional[Dict] = None
100
+
101
+ @dataclass
102
+ class SearchResultComments:
103
+ question: List[StackOverflowComment]
104
+ answers: Dict[int, List[StackOverflowComment]]
105
+
106
+ @dataclass
107
+ class SearchResult:
108
+ question: StackOverflowQuestion
109
+ answers: List[StackOverflowAnswer]
110
+ comments: Optional[SearchResultComments] = None
test_live_demo.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Live Demo Test - Stack Overflow MCP Server
4
+ This script makes actual API calls to demonstrate the working functionality.
5
+ """
6
+
7
+ import sys
8
+ import os
9
+ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
10
+
11
+ from gradio_app import search_by_query_sync, search_by_error_sync, get_question_sync
12
+
13
+ def test_live_functionality():
14
+ """Test the actual functionality of our Stack Overflow MCP server."""
15
+
16
+ print("🧪 LIVE FUNCTIONALITY TEST")
17
+ print("=" * 60)
18
+
19
+ # Test 1: General Search
20
+ print("\n1️⃣ Testing General Search...")
21
+ print("Query: 'python list comprehension'")
22
+ print("Tags: 'python'")
23
+ print("-" * 40)
24
+
25
+ try:
26
+ result = search_by_query_sync(
27
+ query="python list comprehension",
28
+ tags="python",
29
+ min_score=10,
30
+ has_accepted_answer=True,
31
+ limit=2,
32
+ response_format="markdown"
33
+ )
34
+ print("✅ SUCCESS:")
35
+ print(result[:500] + "..." if len(result) > 500 else result)
36
+ except Exception as e:
37
+ print(f"❌ ERROR: {e}")
38
+
39
+ print("\n" + "=" * 60)
40
+
41
+ # Test 2: Error Search
42
+ print("\n2️⃣ Testing Error Search...")
43
+ print("Error: 'TypeError: NoneType'")
44
+ print("Language: 'python'")
45
+ print("-" * 40)
46
+
47
+ try:
48
+ result = search_by_error_sync(
49
+ error_message="TypeError: NoneType object has no attribute",
50
+ language="python",
51
+ technologies="",
52
+ min_score=5,
53
+ has_accepted_answer=True,
54
+ limit=2,
55
+ response_format="markdown"
56
+ )
57
+ print("✅ SUCCESS:")
58
+ print(result[:500] + "..." if len(result) > 500 else result)
59
+ except Exception as e:
60
+ print(f"❌ ERROR: {e}")
61
+
62
+ print("\n" + "=" * 60)
63
+
64
+ # Test 3: Get Question
65
+ print("\n3️⃣ Testing Get Question...")
66
+ print("Question ID: 11227809 (Famous sorting question)")
67
+ print("-" * 40)
68
+
69
+ try:
70
+ result = get_question_sync(
71
+ question_id="11227809",
72
+ include_comments=False,
73
+ response_format="markdown"
74
+ )
75
+ print("✅ SUCCESS:")
76
+ print(result[:500] + "..." if len(result) > 500 else result)
77
+ except Exception as e:
78
+ print(f"❌ ERROR: {e}")
79
+
80
+ print("\n" + "=" * 60)
81
+ print("🎯 Live functionality test completed!")
82
+ print("🚀 All functions are working and connected to Stack Overflow API")
83
+
84
+ if __name__ == "__main__":
85
+ test_live_functionality()
tests/api/test_search.py ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pytest
3
+ import asyncio
4
+ import httpx
5
+ from unittest.mock import patch, MagicMock, AsyncMock
6
+ from dotenv import load_dotenv
7
+
8
+ from stackoverflow_mcp.api import StackExchangeAPI
9
+ from stackoverflow_mcp.types import SearchResult
10
+
11
+ # Load test environment variables
12
+ load_dotenv(".env.test")
13
+
14
+
15
+ @pytest.fixture
16
+ def api_key():
17
+ """Return API key from environment or None."""
18
+ return os.getenv("STACK_EXCHANGE_API_KEY")
19
+
20
+
21
+ @pytest.fixture
22
+ def api(api_key):
23
+ """Create a StackExchangeAPI instance for testing."""
24
+ api = StackExchangeAPI(api_key=api_key)
25
+ yield api
26
+ # Clean up
27
+ asyncio.run(api.close())
28
+
29
+
30
+ @pytest.fixture
31
+ def mock_search_response():
32
+ """Create a mock search response."""
33
+ response = MagicMock()
34
+ response.raise_for_status = MagicMock()
35
+ response.json = MagicMock(return_value={
36
+ "items": [
37
+ {
38
+ "question_id": 12345,
39
+ "title": "How to unittest a Flask application?",
40
+ "body": "<p>I'm trying to test my Flask application with unittest.</p>",
41
+ "score": 25,
42
+ "answer_count": 3,
43
+ "is_answered": True,
44
+ "accepted_answer_id": 54321,
45
+ "creation_date": 1609459200,
46
+ "last_activity_date": 1609459200,
47
+ "view_count": 1000,
48
+ "tags": ["python", "flask", "testing", "unittest"],
49
+ "link": "https://stackoverflow.com/q/12345",
50
+ "closed_date": None,
51
+ "owner": {
52
+ "user_id": 101,
53
+ "display_name": "Test User",
54
+ "reputation": 1000
55
+ }
56
+ }
57
+ ],
58
+ "has_more": False,
59
+ "quota_max": 300,
60
+ "quota_remaining": 299
61
+ })
62
+ return response
63
+
64
+
65
+ # REAL API TESTS
66
+
67
+ @pytest.mark.asyncio
68
+ @pytest.mark.real_api
69
+ async def test_search_by_query_real(api):
70
+ """Test searching by query using real API."""
71
+ # Skip if no API key
72
+ if not os.getenv("STACK_EXCHANGE_API_KEY"):
73
+ pytest.skip("API key required for real API tests")
74
+
75
+ results = await api.search_by_query(
76
+ query="python unittest flask",
77
+ tags=["python", "flask"],
78
+ limit=3
79
+ )
80
+
81
+ # Basic validation
82
+ assert isinstance(results, list)
83
+ if results: # May be empty if no results match
84
+ assert isinstance(results[0], SearchResult)
85
+ assert results[0].question.title is not None
86
+ assert "python" in [tag.lower() for tag in results[0].question.tags]
87
+
88
+
89
+ @pytest.mark.asyncio
90
+ @pytest.mark.real_api
91
+ async def test_advanced_search_real(api):
92
+ """Test advanced search using real API."""
93
+ # Skip if no API key
94
+ if not os.getenv("STACK_EXCHANGE_API_KEY"):
95
+ pytest.skip("API key required for real API tests")
96
+
97
+ results = await api.advanced_search(
98
+ query="database connection",
99
+ tags=["python"],
100
+ min_score=10,
101
+ has_accepted_answer=True,
102
+ limit=2
103
+ )
104
+
105
+ # Basic validation
106
+ assert isinstance(results, list)
107
+ if results: # May be empty if no results match
108
+ assert isinstance(results[0], SearchResult)
109
+ assert results[0].question.score >= 10
110
+ assert "python" in [tag.lower() for tag in results[0].question.tags]
111
+
112
+
113
+ # MOCK TESTS
114
+
115
+ @pytest.mark.asyncio
116
+ async def test_search_by_query_mock(api, mock_search_response):
117
+ """Test searching by query with mocked response."""
118
+ with patch.object(httpx.AsyncClient, 'get', return_value=mock_search_response):
119
+ results = await api.search_by_query(
120
+ query="flask unittest",
121
+ tags=["python", "flask"],
122
+ min_score=10,
123
+ limit=5
124
+ )
125
+
126
+ assert len(results) == 1
127
+ assert results[0].question.question_id == 12345
128
+ assert results[0].question.title == "How to unittest a Flask application?"
129
+ assert "python" in results[0].question.tags
130
+ assert "flask" in results[0].question.tags
131
+
132
+
133
+ @pytest.mark.asyncio
134
+ async def test_empty_search_results(api):
135
+ """Test empty search results handling."""
136
+ empty_response = MagicMock()
137
+ empty_response.raise_for_status = MagicMock()
138
+ empty_response.json = MagicMock(return_value={"items": []})
139
+
140
+ with patch.object(httpx.AsyncClient, 'get', return_value=empty_response):
141
+ results = await api.search_by_query(
142
+ query="definitely will not find anything 89797979",
143
+ limit=1
144
+ )
145
+
146
+ assert isinstance(results, list)
147
+ assert len(results) == 0
148
+
149
+
150
+ @pytest.mark.asyncio
151
+ async def test_search_with_min_score_filtering(api, mock_search_response):
152
+ """Test that min_score parameter properly filters results."""
153
+ with patch.object(httpx.AsyncClient, 'get', return_value=mock_search_response):
154
+ # Should return results (mock score is 25)
155
+ results_included = await api.search_by_query(
156
+ query="flask unittest",
157
+ min_score=20,
158
+ limit=5
159
+ )
160
+ assert len(results_included) == 1
161
+
162
+ # Should filter out results
163
+ results_filtered = await api.search_by_query(
164
+ query="flask unittest",
165
+ min_score=30,
166
+ limit=5
167
+ )
168
+ assert len(results_filtered) == 0
169
+
170
+
171
+ @pytest.mark.asyncio
172
+ async def test_search_with_multiple_tags(api, mock_search_response):
173
+ """Test searching with multiple tags."""
174
+ with patch.object(httpx.AsyncClient, 'get', return_value=mock_search_response):
175
+ results = await api.search_by_query(
176
+ query="test",
177
+ tags=["python", "flask", "django"],
178
+ limit=5
179
+ )
180
+
181
+ # Verify the tags were properly passed to the request
182
+ call_args = httpx.AsyncClient.get.call_args
183
+ params = call_args[1]['params']
184
+ assert 'tagged' in params
185
+ assert params['tagged'] == "python;flask;django"
186
+
187
+
188
+ @pytest.mark.asyncio
189
+ async def test_search_with_excluded_tags(api, mock_search_response):
190
+ """Test searching with excluded tags."""
191
+ with patch.object(httpx.AsyncClient, 'get', return_value=mock_search_response):
192
+ results = await api.search_by_query(
193
+ query="test",
194
+ excluded_tags=["javascript", "c#"],
195
+ limit=5
196
+ )
197
+
198
+ # Verify the excluded tags were properly passed to the request
199
+ call_args = httpx.AsyncClient.get.call_args
200
+ params = call_args[1]['params']
201
+ assert 'nottagged' in params
202
+ assert params['nottagged'] == "javascript;c#"
203
+
204
+
205
+ @pytest.mark.asyncio
206
+ async def test_search_with_advanced_parameters(api, mock_search_response):
207
+ """Test advanced search with multiple parameters."""
208
+ with patch.object(httpx.AsyncClient, 'get', return_value=mock_search_response):
209
+ results = await api.advanced_search(
210
+ query="flask test",
211
+ tags=["python"],
212
+ title="unittest",
213
+ has_accepted_answer=True,
214
+ sort_by="relevance",
215
+ limit=5
216
+ )
217
+
218
+ # Verify parameters were properly passed
219
+ call_args = httpx.AsyncClient.get.call_args
220
+ params = call_args[1]['params']
221
+ assert params['q'] == "flask test"
222
+ assert params['tagged'] == "python"
223
+ assert params['title'] == "unittest"
224
+ assert params['accepted'] == "true"
225
+ assert params['sort'] == "relevance"
226
+ assert params['pagesize'] == "5"
227
+
228
+
229
+ @pytest.mark.asyncio
230
+ async def test_search_api_error(api):
231
+ """Test handling of API errors."""
232
+ error_response = MagicMock()
233
+ error_response.raise_for_status = MagicMock(
234
+ side_effect=httpx.HTTPStatusError(
235
+ "500 Internal Server Error",
236
+ request=MagicMock(),
237
+ response=MagicMock(status_code=500)
238
+ )
239
+ )
240
+
241
+ with patch.object(httpx.AsyncClient, 'get', return_value=error_response):
242
+ with pytest.raises(httpx.HTTPStatusError):
243
+ await api.search_by_query("test query")
tests/test_formatter.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from stackoverflow_mcp.formatter import format_response, clean_html
3
+ from stackoverflow_mcp.types import (
4
+ SearchResult,
5
+ StackOverflowQuestion,
6
+ StackOverflowAnswer,
7
+ StackOverflowComment,
8
+ SearchResultComments
9
+ )
10
+
11
+ @pytest.fixture
12
+ def sample_result():
13
+ """Create a sample search result for testing."""
14
+ question = StackOverflowQuestion(
15
+ question_id=12345,
16
+ title="How to test Python code?",
17
+ body="<p>I'm trying to test my <code>Python</code> code.</p><pre><code>def test():\n pass</code></pre>",
18
+ score=10,
19
+ answer_count=2,
20
+ is_answered=True,
21
+ accepted_answer_id=54321,
22
+ tags=["python", "testing"],
23
+ link="https://stackoverflow.com/q/12345"
24
+ )
25
+
26
+ answers = [
27
+ StackOverflowAnswer(
28
+ answer_id=54321,
29
+ question_id=12345,
30
+ score=5,
31
+ is_accepted=True,
32
+ body="<p>You should use <code>pytest</code>.</p>",
33
+ link="https://stackoverflow.com/a/54321"
34
+ ),
35
+ StackOverflowAnswer(
36
+ answer_id=67890,
37
+ question_id=12345,
38
+ score=3,
39
+ is_accepted=False,
40
+ body="<p>Another option is <code>unittest</code>.</p>",
41
+ link="https://stackoverflow.com/a/67890"
42
+ )
43
+ ]
44
+
45
+ comments = SearchResultComments(
46
+ question=[
47
+ StackOverflowComment(
48
+ comment_id=111,
49
+ post_id=12345,
50
+ score=2,
51
+ body="Have you tried pytest?"
52
+ )
53
+ ],
54
+ answers={
55
+ 54321: [
56
+ StackOverflowComment(
57
+ comment_id=222,
58
+ post_id=54321,
59
+ score=1,
60
+ body="Great answer!"
61
+ )
62
+ ],
63
+ 67890: []
64
+ }
65
+ )
66
+
67
+ return SearchResult(
68
+ question=question,
69
+ answers=answers,
70
+ comments=comments
71
+ )
72
+
73
+ def test_clean_html():
74
+ """Test HTML cleaning."""
75
+ html = "<p>This is <b>bold</b> and <i>italic</i>.</p><code>inline code</code><pre><code>def function():\n return True</code></pre>"
76
+ cleaned = clean_html(html)
77
+
78
+ assert "<p>" not in cleaned
79
+ assert "<b>" not in cleaned
80
+ assert "<i>" not in cleaned
81
+ assert "This is bold and italic." in cleaned
82
+ assert "`inline code`" in cleaned
83
+ assert "```\ndef function():\n return True\n```" in cleaned
84
+
85
+ def test_format_response_markdown(sample_result):
86
+ """Test formatting as Markdown."""
87
+ markdown = format_response([sample_result], "markdown")
88
+
89
+ assert "# How to test Python code?" in markdown
90
+ assert "**Score:** 10" in markdown
91
+ assert "## Question" in markdown
92
+ assert "I'm trying to test my `Python` code." in markdown
93
+ assert "```\ndef test():\n pass\n```" in markdown
94
+ assert "### Question Comments" in markdown
95
+ assert "- Have you tried pytest?" in markdown
96
+ assert "### ✓ Answer (Score: 5)" in markdown
97
+ assert "You should use `pytest`." in markdown
98
+ assert "### Answer (Score: 3)" in markdown
99
+ assert "Another option is `unittest`." in markdown
100
+ assert "[View on Stack Overflow](https://stackoverflow.com/q/12345)" in markdown
101
+
102
+ def test_format_response_json(sample_result):
103
+ """Test formatting as JSON."""
104
+ json_str = format_response([sample_result], "json")
105
+
106
+ assert "How to test Python code?" in json_str
107
+ assert "question_id" in json_str
108
+ assert "answers" in json_str
109
+ assert "comments" in json_str
tests/test_general_api_health.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import pytest
3
+ import asyncio
4
+ import httpx
5
+ from unittest.mock import patch, MagicMock
6
+ from stackoverflow_mcp.api import StackExchangeAPI
7
+ from stackoverflow_mcp.types import StackOverflowQuestion, StackOverflowAnswer, SearchResult
8
+
9
+
10
+ @pytest.fixture
11
+ def api():
12
+ """Create A StackExchangeAPI instance for testing
13
+ """
14
+ return StackExchangeAPI(api_key="test_key")
15
+
16
+ @pytest.fixture
17
+ def mock_response():
18
+ """Create a mock response for httpx."""
19
+
20
+ response = MagicMock()
21
+ response.raise_for_status = MagicMock()
22
+ response.json = MagicMock(return_value={
23
+ "items" : [
24
+ {
25
+ "question_id": 12345,
26
+ "title": "Test Question",
27
+ "body": "Test body",
28
+ "score": 10,
29
+ "answer_count": 2,
30
+ "is_answered": True,
31
+ "accepted_answer_id": 54321,
32
+ "creation_date": 1609459200,
33
+ "tags": ["python", "testing"],
34
+ "link": "https://stackoverflow.com/q/12345"
35
+ }
36
+ ]
37
+ })
38
+ return response
39
+
40
+ @pytest.mark.asyncio
41
+ async def test_search_by_query(api, mock_response):
42
+ """Test searching by query."""
43
+ with patch.object(httpx.AsyncClient, 'get', return_value=mock_response):
44
+ results = await api.search_by_query("test query")
45
+
46
+ assert len(results) == 1
47
+ assert results[0].question.question_id == 12345
48
+ assert results[0].question.title == "Test Question"
49
+ assert isinstance(results[0], SearchResult)
50
+
51
+ @pytest.mark.asyncio
52
+ async def test_get_question(api, mock_response):
53
+ """Test getting a specific question."""
54
+ with patch.object(httpx.AsyncClient, 'get', return_value=mock_response):
55
+ result = await api.get_question(12345)
56
+
57
+ assert result.question.question_id == 12345
58
+ assert result.question.title == "Test Question"
59
+ assert isinstance(result, SearchResult)
60
+
61
+ @pytest.mark.asyncio
62
+ async def test_rate_limiting(api):
63
+ """Test rate limiting mechanism."""
64
+ mock_resp = MagicMock()
65
+ mock_resp.raise_for_status = MagicMock()
66
+ mock_resp.json = MagicMock(return_value={"items": []})
67
+
68
+ with patch.object(httpx.AsyncClient, 'get', return_value=mock_resp):
69
+ api.request_timestamps = [time.time() * 1000 * 1000 for _ in range(30)]
70
+
71
+ with patch('asyncio.sleep') as mock_sleep:
72
+ try:
73
+ await api.search_by_query("test")
74
+ except Exception as e:
75
+ assert str(e) == "Maximum rate limiting attempts exceeded"
76
+ mock_sleep.assert_called()
77
+
78
+ @pytest.mark.asyncio
79
+ async def test_retry_after_429(api):
80
+ """Test retry behavior after hitting rate limit."""
81
+ error_resp = MagicMock()
82
+ error_resp.raise_for_status.side_effect = httpx.HTTPStatusError("Rate limited", request=MagicMock(), response=MagicMock(status_code=429))
83
+
84
+ success_resp = MagicMock()
85
+ success_resp.raise_for_status = MagicMock()
86
+ success_resp.json = MagicMock(return_value={"items": []})
87
+
88
+ with patch.object(httpx.AsyncClient, 'get', side_effect=[error_resp, success_resp]):
89
+ with patch('asyncio.sleep') as mock_sleep:
90
+ await api.search_by_query("test", retries=1)
91
+ mock_sleep.assert_called_once()
tests/test_server.py ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ import asyncio
3
+ from unittest.mock import patch, MagicMock, AsyncMock
4
+
5
+ from stackoverflow_mcp.server import mcp, search_by_query, search_by_error, get_question, analyze_stack_trace
6
+ from stackoverflow_mcp.types import StackOverflowQuestion, StackOverflowAnswer, SearchResult
7
+ from mcp.server.fastmcp import Context
8
+
9
+ @pytest.fixture
10
+ def mock_context():
11
+ """Create a mock context for testing"""
12
+ context = MagicMock(spec=Context)
13
+
14
+ context.debug = MagicMock()
15
+ context.info = MagicMock()
16
+ context.error = MagicMock()
17
+ context.request_context.lifespan_context.api = AsyncMock()
18
+
19
+
20
+ return context
21
+
22
+ @pytest.fixture
23
+ def mock_search_result():
24
+ """Create a mock search result for testing"""
25
+ question = StackOverflowQuestion(
26
+ question_id=12345,
27
+ title="Test Question",
28
+ body="Test body",
29
+ score=10,
30
+ answer_count=2,
31
+ is_answered=True,
32
+ accepted_answer_id=54321,
33
+ creation_date=1609459200,
34
+ tags=["python", "testing"],
35
+ link="https://stackoverflow.com/q/12345"
36
+ )
37
+
38
+ answer = StackOverflowAnswer(
39
+ answer_id=54321,
40
+ question_id=12345,
41
+ score=5,
42
+ is_accepted=True,
43
+ body="Test answer",
44
+ creation_date=1609459300,
45
+ link="https://stackoverflow.com/a/54321"
46
+ )
47
+
48
+ return SearchResult(
49
+ question=question,
50
+ answers=[answer],
51
+ comments=None
52
+ )
53
+
54
+ @pytest.mark.asyncio
55
+ async def test_search_by_query(mock_context, mock_search_result):
56
+ """Test search by query function"""
57
+ mock_context.request_context.lifespan_context.api.search_by_query.return_value = [mock_search_result]
58
+
59
+
60
+ result = await search_by_query(
61
+ query="test query",
62
+ tags=["python"],
63
+ min_score=5,
64
+ include_comments=False,
65
+ response_format="markdown",
66
+ limit=5,
67
+ ctx=mock_context
68
+ )
69
+
70
+ mock_context.request_context.lifespan_context.api.search_by_query.assert_called_once_with(
71
+ query="test query",
72
+ tags=["python"],
73
+ min_score=5,
74
+ limit=5,
75
+ include_comments=False
76
+ )
77
+
78
+ assert "Test Question" in result
79
+
80
+ @pytest.mark.asyncio
81
+ async def test_search_by_error(mock_context, mock_search_result):
82
+ """Test search by error function"""
83
+ mock_context.request_context.lifespan_context.api.search_by_query.return_value = [mock_search_result]
84
+
85
+ result = await search_by_error(
86
+ error_message="test error",
87
+ language="python",
88
+ technologies=["django"],
89
+ min_score=5,
90
+ include_comments=False,
91
+ response_format="markdown",
92
+ limit=5,
93
+ ctx=mock_context
94
+ )
95
+
96
+ mock_context.request_context.lifespan_context.api.search_by_query.assert_called_once_with(
97
+ query="test error",
98
+ tags=["python", "django"],
99
+ min_score=5,
100
+ limit=5,
101
+ include_comments=False
102
+ )
103
+
104
+ assert "Test Question" in result
105
+
106
+ @pytest.mark.asyncio
107
+ async def test_get_question(mock_context, mock_search_result):
108
+ """Test get question function"""
109
+ mock_context.request_context.lifespan_context.api.get_question.return_value = mock_search_result
110
+
111
+ result = await get_question(
112
+ question_id=12345,
113
+ include_comments=True,
114
+ response_format="markdown",
115
+ ctx=mock_context
116
+ )
117
+
118
+ mock_context.request_context.lifespan_context.api.get_question.assert_called_once_with(
119
+ question_id=12345,
120
+ include_comments=True
121
+ )
122
+
123
+ assert "Test Question" in result
124
+
125
+ @pytest.mark.asyncio
126
+ async def test_analyze_stack_trace(mock_context, mock_search_result):
127
+ """Test analyze stack trace function"""
128
+ mock_context.request_context.lifespan_context.api.search_by_query.return_value = [mock_search_result]
129
+
130
+ result = await analyze_stack_trace(
131
+ stack_trace="Error: Something went wrong\n at Function.Module._resolveFilename",
132
+ language="javascript",
133
+ include_comments=True,
134
+ response_format="markdown",
135
+ limit=3,
136
+ ctx=mock_context
137
+ )
138
+
139
+ mock_context.request_context.lifespan_context.api.search_by_query.assert_called_once_with(
140
+ query="Error: Something went wrong",
141
+ tags=["javascript"],
142
+ min_score=0,
143
+ limit=3,
144
+ include_comments=True
145
+ )
146
+
147
+ assert "Test Question" in result
uv.lock ADDED
The diff for this file is too large to render. See raw diff