# Directory Structure
```
├── .gitattributes
├── .github
│ └── FUNDING.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── package-lock.json
├── package.json
├── public
│ └── assets
│ ├── gcp.png
│ ├── openrouter.png
│ └── preview.png
├── README.md
├── smithery.yaml
├── src
│ └── index.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
```
1 | build/* linguist-vendored
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Node .gitignore
2 | # Logs
3 | logs
4 | *.log
5 | npm-debug.log*
6 | yarn-debug.log*
7 | yarn-error.log*
8 | lerna-debug.log*
9 | .pnpm-debug.log*
10 |
11 | # Diagnostic reports (https://nodejs.org/api/report.html)
12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
13 |
14 | # Runtime data
15 | pids
16 | *.pid
17 | *.seed
18 | *.pid.lock
19 |
20 | # Directory for instrumented libs generated by jscoverage/JSCover
21 | lib-cov
22 |
23 | # Coverage directory used by tools like istanbul
24 | coverage
25 | *.lcov
26 |
27 | # nyc test coverage
28 | .nyc_output
29 |
30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
31 | .grunt
32 |
33 | # Bower dependency directory (https://bower.io/)
34 | bower_components
35 |
36 | # node-waf configuration
37 | .lock-wscript
38 |
39 | # Compiled binary addons (https://nodejs.org/api/addons.html)
40 | build/Release
41 |
42 | # Dependency directories
43 | node_modules/
44 | jspm_packages/
45 |
46 | # Snowpack dependency directory (https://snowpack.dev/)
47 | web_modules/
48 |
49 | # TypeScript cache
50 | *.tsbuildinfo
51 |
52 | # Optional npm cache directory
53 | .npm
54 |
55 | # Optional npm build directory
56 | build/
57 |
58 | # Optional eslint cache
59 | .eslintcache
60 |
61 | # Optional stylelint cache
62 | .stylelintcache
63 |
64 | # Microbundle cache
65 | .rpt2_cache/
66 | .rts2_cache_cjs/
67 | .rts2_cache_es/
68 | .rts2_cache_umd/
69 |
70 | # Optional REPL history
71 | .node_repl_history
72 |
73 | # Output of 'npm pack'
74 | *.tgz
75 |
76 | # Yarn Integrity file
77 | .yarn-integrity
78 |
79 | # dotenv environment variable files
80 | .env
81 | .env.development.local
82 | .env.test.local
83 | .env.production.local
84 | .env.local
85 |
86 | # parcel-bundler cache (https://parceljs.org/)
87 | .cache
88 | .parcel-cache
89 |
90 | # Next.js build output
91 | .next
92 | out
93 |
94 | # Nuxt.js build / generate output
95 | .nuxt
96 | dist
97 |
98 | # Gatsby files
99 | .cache/
100 | # Comment in the public line in if your project uses Gatsby and not Next.js
101 | # https://nextjs.org/blog/next-9-1#public-directory-support
102 | # public
103 |
104 | # vuepress build output
105 | .vuepress/dist
106 |
107 | # vuepress v2.x temp and cache directory
108 | .temp
109 | .cache
110 |
111 | # Docusaurus cache and generated files
112 | .docusaurus
113 |
114 | # Serverless directories
115 | .serverless/
116 |
117 | # FuseBox cache
118 | .fusebox/
119 |
120 | # DynamoDB Local files
121 | .dynamodb/
122 |
123 | # TernJS port file
124 | .tern-port
125 |
126 | # Stores VSCode versions used for testing VSCode extensions
127 | .vscode-test
128 |
129 | # yarn v2
130 | .yarn/cache
131 | .yarn/unplugged
132 | .yarn/build-state.yml
133 | .yarn/install-state.gz
134 | .pnp.*
135 |
136 | # macOS .gitignore
137 | # General
138 | .DS_Store
139 | .AppleDouble
140 | .LSOverride
141 |
142 | # Icon must end with two \r
143 | Icon
144 | Icon?
145 |
146 | # Thumbnails
147 | ._*
148 |
149 | # Files that might appear in the root of a volume
150 | .DocumentRevisions-V100
151 | .fseventsd
152 | .Spotlight-V100
153 | .TemporaryItems
154 | .Trashes
155 | .VolumeIcon.icns
156 | .com.apple.timemachine.donotpresent
157 |
158 | # Directories potentially created on remote AFP share
159 | .AppleDB
160 | .AppleDesktop
161 | Network Trash Folder
162 | Temporary Items
163 | .apdisk
164 |
165 | # Windows .gitignore
166 | # Windows thumbnail cache files
167 | Thumbs.db
168 | Thumbs.db:encryptable
169 | ehthumbs.db
170 | ehthumbs_vista.db
171 |
172 | # Dump file
173 | *.stackdump
174 |
175 | # Folder config file
176 | [Dd]esktop.ini
177 |
178 | # Recycle Bin used on file shares
179 | $RECYCLE.BIN/
180 |
181 | # Windows Installer files
182 | *.cab
183 | *.msi
184 | *.msix
185 | *.msm
186 | *.msp
187 |
188 | # Windows shortcuts
189 | *.lnk
190 |
191 | # Linux .gitignore
192 | # gitginore template for creating Snap packages
193 | # website: https://snapcraft.io/
194 |
195 | parts/
196 | prime/
197 | stage/
198 | *.snap
199 |
200 | # Snapcraft global state tracking data(automatically generated)
201 | # https://forum.snapcraft.io/t/location-to-save-global-state/768
202 | /snap/.snapcraft/
203 |
204 | # Source archive packed by `snapcraft cleanbuild` before pushing to the LXD container
205 | /*_source.tar.bz2
206 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # MCP Status Observer
2 | [](https://mseep.ai/app/d7d5a94b-3378-479b-b5a3-35efa8904d2e) [](https://archestra.ai/mcp-catalog/imprvhub__mcp-status-observer)
3 | [](https://smithery.ai/server/@imprvhub/mcp-status-observer)
4 |
5 | <table style="border-collapse: collapse; width: 100%; table-layout: fixed;">
6 | <tr>
7 | <td style="padding: 15px; vertical-align: middle; border: none; text-align: center;">
8 | <a href="https://mseep.ai/app/imprvhub-mcp-status-observer">
9 | <img src="https://mseep.net/pr/imprvhub-mcp-status-observer-badge.png" alt="MseeP.ai Security Assessment Badge" />
10 | </a>
11 | </td>
12 | <td style="width: 40%; padding: 15px; vertical-align: middle; border: none;">An integration that allows Claude Desktop to monitor and query the operational status of major digital platforms including AI providers, cloud services, and developer tools using the Model Context Protocol (MCP).</td>
13 | <td style="width: 60%; padding: 0; vertical-align: middle; border: none; min-width: 300px; text-align: center;">
14 | <a href="https://glama.ai/mcp/servers/@imprvhub/mcp-status-observer">
15 | <img style="max-width: 100%; height: auto; min-width: 300px;" src="https://glama.ai/mcp/servers/@imprvhub/mcp-status-observer/badge" alt="Status Observer MCP server" />
16 | </a>
17 | </td>
18 |
19 | </tr>
20 | </table>
21 |
22 | > [!IMPORTANT]
23 | > This project is continuously updated with new platform integrations. If you're not seeing a service that should be available, or if Claude doesn't recognize a platform, please update by running `npm run build` from a freshly cloned repository.
24 | >
25 | > **Last updated**: 2025-09-12T07:22:15Z (UTC) - Added OpenRouter status integration with RSS incident tracking
26 |
27 | ## Features
28 |
29 | - Monitor world's most used digital platforms (GitHub, Slack, Discord, etc.)
30 | - Track AI providers including OpenRouter, OpenAI, Anthropic, and Gemini
31 | - Get detailed status information for specific services with incident history
32 | - Check status of specific components within each platform
33 | - Real-time updates of service status with impact analysis
34 | - Comprehensive incident tracking with resolution status and timelines
35 | - Simple query interface with commands like `status --openrouter`
36 |
37 | ## Demo
38 |
39 | <p>
40 | <a href="https://www.youtube.com/watch?v=EV1ac0PMzKg">
41 | <img src="public/assets/preview.png" width="600" alt="Status Observer MCP Demo">
42 | </a>
43 | </p>
44 |
45 | <details>
46 | <summary> Timestamps </summary>
47 |
48 | Click on any timestamp to jump to that section of the video
49 |
50 | [**00:00**](https://www.youtube.com/watch?v=EV1ac0PMzKg&t=0s) - **LinkedIn Platform Status Assessment**
51 | Comprehensive analysis of LinkedIn's operational health, including detailed examination of core services such as LinkedIn.com, LinkedIn Learning, Campaign Manager, Sales Navigator, Recruiter, and Talent solutions. All systems confirmed fully operational with zero service disruptions.
52 |
53 | [**00:20**](https://www.youtube.com/watch?v=EV1ac0PMzKg&t=20s) - **GitHub Infrastructure Status Overview**
54 | Detailed evaluation of GitHub's service availability, covering critical components including Git operations, API requests, Actions, Webhooks, Issues, Pull Requests, Packages, Pages, Codespaces, and Copilot functionality. Complete operational status confirmed across all GitHub services.
55 |
56 | [**00:40**](https://www.youtube.com/watch?v=EV1ac0PMzKg&t=40s) - **Vercel Platform Reliability Analysis**
57 | In-depth examination of Vercel's global edge network and deployment infrastructure, featuring comprehensive status reporting on core services such as API, Dashboard, Builds, Serverless Functions, Edge Functions, and global CDN locations. All Vercel services verified operational across all regions.
58 |
59 | [**01:08**](https://www.youtube.com/watch?v=EV1ac0PMzKg&t=68s) - **Cloudflare Network Status Examination**
60 | Extensive analysis of Cloudflare's global infrastructure status, detailing service availability across geographic regions and specific service components. Identified performance degradation in multiple regions (Africa, Asia, Europe, Latin America, Middle East, North America) while core services remain functional. Includes detailed assessment of regional data centers under maintenance and technical impact analysis.
61 |
62 | [**01:46**](https://www.youtube.com/watch?v=EV1ac0PMzKg&t=106s) - **Global Operational Status Report**
63 | Consolidated overview of operational status across all major technology platforms and service providers, highlighting both fully operational services (GitHub, Vercel, Netlify, Asana, Atlassian, OpenRouter, etc.) and services experiencing degraded performance (Cloudflare, Twilio). Includes strategic recommendations for organizations with dependencies on affected services.
64 | </details>
65 |
66 | ## Requirements
67 |
68 | - Node.js 16 or higher
69 | - Claude Desktop
70 | - Internet connection to access status APIs
71 |
72 | ## Installation
73 |
74 | ### Installing Manually
75 | 1. Clone or download this repository:
76 | ```bash
77 | git clone https://github.com/imprvhub/mcp-status-observer
78 | cd mcp-status-observer
79 | ```
80 |
81 | 2. Install dependencies:
82 | ```bash
83 | npm install
84 | ```
85 |
86 | 3. Build the project:
87 | ```bash
88 | npm run build
89 | ```
90 |
91 | ## Running the MCP Server
92 |
93 | There are two ways to run the MCP server:
94 |
95 | ### Option 1: Running manually
96 |
97 | 1. Open a terminal or command prompt
98 | 2. Navigate to the project directory
99 | 3. Run the server directly:
100 |
101 | ```bash
102 | node build/index.js
103 | ```
104 |
105 | Keep this terminal window open while using Claude Desktop. The server will run until you close the terminal.
106 |
107 | ### Option 2: Auto-starting with Claude Desktop (recommended for regular use)
108 |
109 | The Claude Desktop can automatically start the MCP server when needed. To set this up:
110 |
111 | #### Configuration
112 |
113 | The Claude Desktop configuration file is located at:
114 |
115 | - **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
116 | - **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
117 | - **Linux**: `~/.config/Claude/claude_desktop_config.json`
118 |
119 | Edit this file to add the Status Observer MCP configuration. If the file doesn't exist, create it:
120 |
121 | ```json
122 | {
123 | "mcpServers": {
124 | "statusObserver": {
125 | "command": "node",
126 | "args": ["ABSOLUTE_PATH_TO_DIRECTORY/mcp-status-observer/build/index.js"]
127 | }
128 | }
129 | }
130 | ```
131 |
132 | **Important**: Replace `ABSOLUTE_PATH_TO_DIRECTORY` with the **complete absolute path** where you installed the MCP
133 | - macOS/Linux example: `/Users/username/mcp-status-observer`
134 | - Windows example: `C:\\Users\\username\\mcp-status-observer`
135 |
136 | If you already have other MCPs configured, simply add the "statusObserver" section inside the "mcpServers" object. Here's an example of a configuration with multiple MCPs:
137 |
138 | ```json
139 | {
140 | "mcpServers": {
141 | "otherMcp1": {
142 | "command": "...",
143 | "args": ["..."]
144 | },
145 | "otherMcp2": {
146 | "command": "...",
147 | "args": ["..."]
148 | },
149 | "statusObserver": {
150 | "command": "node",
151 | "args": [
152 | "ABSOLUTE_PATH_TO_DIRECTORY/mcp-status-observer/build/index.js"
153 | ]
154 | }
155 | }
156 | }
157 | ```
158 |
159 | The MCP server will automatically start when Claude Desktop needs it, based on the configuration in your `claude_desktop_config.json` file.
160 |
161 | ## Usage
162 |
163 | 1. Restart Claude Desktop after modifying the configuration
164 | 2. In Claude, use the `status` command to interact with the Status Observer MCP Server
165 | 3. The MCP server runs as a subprocess managed by Claude Desktop
166 |
167 | ## Available Commands
168 |
169 | The Status Observer MCP provides a single tool named `status` with several commands:
170 |
171 | | Command | Description | Parameters | Example |
172 | |---------|-------------|------------|---------|
173 | | `list` | List all available platforms | None | `status list` |
174 | | `--[platform]` | Get status for a specific platform | Platform name | `status --openrouter` |
175 | | `--all` | Get status for all platforms | None | `status --all` |
176 |
177 | ## Supported Platforms
178 |
179 | The Status Observer monitors 22 major digital platforms across various categories:
180 |
181 | ### AI & Machine Learning (4)
182 | - **OpenRouter** - AI model routing and access platform
183 | - **OpenAI** - Leading AI services provider (ChatGPT, DALL-E, API)
184 | - **Anthropic** - AI assistant provider (Claude)
185 | - **Gemini** - Google's multimodal AI platform
186 |
187 | ### Cloud Infrastructure (4)
188 | - **Google Cloud Platform** - Comprehensive cloud computing services
189 | - **DigitalOcean** - Developer-focused cloud infrastructure
190 | - **Vercel** - Frontend deployment and edge platform
191 | - **Netlify** - Web development and deployment platform
192 |
193 | ### Developer Tools & Platforms (5)
194 | - **Docker** - Container platform and services
195 | - **GitHub** - Version control and collaboration platform
196 | - **npm** - JavaScript package manager and registry
197 | - **Atlassian** - Developer collaboration tools (Jira, Bitbucket, Confluence)
198 | - **Supabase** - Open source backend platform (PostgreSQL, auth, storage)
199 |
200 | ### Productivity & Collaboration (5)
201 | - **LinkedIn** - Professional networking platform
202 | - **Slack** - Business communication and collaboration
203 | - **Asana** - Team workflow and project management
204 | - **Dropbox** - Cloud file storage and collaboration
205 | - **X (Twitter)** - Social media and real-time communication
206 |
207 | ### Web Infrastructure & Security (3)
208 | - **Cloudflare** - Web infrastructure, CDN, and security
209 | - **Discord** - Developer community and communication platform
210 | - **Reddit** - Social news and developer community platform
211 |
212 | ### Analytics & Business Tools (1)
213 | - **Amplitude** - Product analytics platform
214 |
215 | ## Example Usage
216 |
217 | Here are various examples of how to use the Status Observer with Claude:
218 |
219 | ### Direct Commands:
220 |
221 | ```
222 | # AI Platforms
223 | status --openrouter
224 | status --openai
225 | status --anthropic
226 | status --gemini
227 |
228 | # Cloud Infrastructure
229 | status --gcp
230 | status --vercel
231 | status --digitalocean
232 | status --netlify
233 |
234 | # Developer Tools
235 | status --docker
236 | status --github
237 | status --atlassian
238 | status --supabase
239 | status --npm
240 |
241 | # Productivity & Social
242 | status --linkedin
243 | status --slack
244 | status --x
245 | status --dropbox
246 |
247 | # Web Infrastructure
248 | status --cloudflare
249 | status --discord
250 |
251 | # All platforms
252 | status --all
253 | status list
254 | ```
255 |
256 | ### Preview
257 | 
258 | 
259 |
260 | ### Natural Language Prompts:
261 |
262 | You can also interact with the MCP using natural language. Claude will interpret these requests and use the appropriate commands:
263 |
264 | - "Could you check if OpenRouter is having any API issues right now?"
265 | - "What's the status of OpenAI's ChatGPT service?"
266 | - "Has there been any recent incidents with Claude or the Anthropic API?"
267 | - "Is Google Cloud Platform experiencing any outages in my region?"
268 | - "Check if Docker Hub is operational for automated builds"
269 | - "What's the current status of LinkedIn's Sales Navigator?"
270 | - "Can you tell me if Google's Gemini AI is experiencing any service disruptions?"
271 | - "Show me the status of all AI platforms including OpenRouter and OpenAI"
272 | - "Are there any active incidents affecting GitHub Actions or Git operations?"
273 | - "Check the overall health of Vercel and Netlify for my deployment pipeline"
274 | - "Has Supabase had any recent database or authentication issues?"
275 | - "What's the status of all major platforms right now?"
276 |
277 |
278 | ## Troubleshooting
279 |
280 | ### "Server disconnected" error
281 | If you see the error "MCP Status Observer: Server disconnected" in Claude Desktop:
282 |
283 | 1. **Verify the server is running**:
284 | - Open a terminal and manually run `node build/index.js` from the project directory
285 | - If the server starts successfully, use Claude while keeping this terminal open
286 |
287 | 2. **Check your configuration**:
288 | - Ensure the absolute path in `claude_desktop_config.json` is correct for your system
289 | - Double-check that you've used double backslashes (`\\`) for Windows paths
290 | - Verify you're using the complete path from the root of your filesystem
291 |
292 | ### Tools not appearing in Claude
293 | If the Status Observer tools don't appear in Claude:
294 | - Make sure you've restarted Claude Desktop after configuration
295 | - Check the Claude Desktop logs for any MCP communication errors
296 | - Ensure the MCP server process is running (run it manually to confirm)
297 | - Verify that the MCP server is correctly registered in the Claude Desktop MCP registry
298 |
299 | ### Checking if the server is running
300 | To check if the server is running:
301 |
302 | - **Windows**: Open Task Manager, go to the "Details" tab, and look for "node.exe"
303 | - **macOS/Linux**: Open Terminal and run `ps aux | grep node`
304 |
305 | If you don't see the server running, start it manually or use the auto-start method.
306 |
307 | ## Contributing
308 |
309 | ### Adding New Status APIs
310 |
311 | Contributors can easily add support for additional platforms by modifying the `initializePlatforms` method in `src/index.ts`. The process is straightforward:
312 |
313 | 1. Identify a platform's status API endpoint
314 | 2. Add a new entry using the `addPlatform` method with the following parameters:
315 | - `id`: A unique identifier for the platform (lowercase, no spaces)
316 | - `name`: The display name of the platform
317 | - `url`: The status API endpoint URL
318 | - `description`: A brief description of the platform
319 |
320 | Example:
321 | ```typescript
322 | this.addPlatform('newservice', 'New Service', 'https://status.newservice.com/api/v2/summary.json', 'Description of the service');
323 | ```
324 |
325 | ### Custom API Integration
326 |
327 | For platforms with non-standard status pages (like OpenRouter, OpenAI, Anthropic), you can create custom handlers:
328 |
329 | 1. Add the platform to `initializePlatforms()`
330 | 2. Create a TypeScript interface for the response format
331 | 3. Add a specific handler method like `getOpenRouterStatus()`
332 | 4. Update the main `getPlatformStatus()` method to route to your handler
333 | 5. Add quick status support in `getQuickPlatformStatus()`
334 |
335 | Example structure for custom handlers:
336 | ```typescript
337 | private async getCustomPlatformStatus(platform: PlatformStatus): Promise<string> {
338 | // Custom parsing logic for your platform
339 | // Return formatted status text
340 | }
341 | ```
342 |
343 | ### Platform Categories
344 |
345 | When adding new platforms, consider organizing them into logical categories:
346 | - **AI/ML**: OpenRouter, OpenAI, Anthropic, Gemini
347 | - **Cloud Infrastructure**: GCP, AWS, Azure, DigitalOcean
348 | - **Developer Tools**: GitHub, GitLab, Docker, npm
349 | - **Productivity**: Slack, Microsoft 365, Google Workspace
350 | - **Web Infrastructure**: Cloudflare, Fastly, Akamai
351 |
352 | ## License
353 |
354 | This project is licensed under the Mozilla Public License 2.0 - see the [LICENSE](https://github.com/imprvhub/mcp-claude-hackernews/blob/main/LICENSE) file for details.
355 |
356 | ## Related Links
357 |
358 | - [Model Context Protocol](https://modelcontextprotocol.io/)
359 | - [Claude Desktop](https://claude.ai/download)
360 | - [MCP Series](https://github.com/mcp-series)
361 |
362 | ## Changelog
363 |
364 | - **2025-09-12**: Added OpenRouter integration with RSS incident tracking and detailed impact analysis
365 | - **2025-04-26**: Added Docker status integration with comprehensive component monitoring
366 | - **2025-03-15**: Enhanced GCP regional status reporting with incident correlation
367 | - **2025-02-28**: Added Anthropic and Gemini AI platform monitoring
368 | - **2025-01-20**: Initial release with core platform support (GitHub, Vercel, Cloudflare, etc.)
369 |
370 | ---
371 |
372 | *Built for the developer community by [imprvhub](https://github.com/imprvhub)*
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "NodeNext",
5 | "moduleResolution": "NodeNext",
6 | "esModuleInterop": true,
7 | "outDir": "./build",
8 | "rootDir": "./src",
9 | "allowJs": true,
10 | "checkJs": false,
11 | "skipLibCheck": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "noImplicitAny": false
14 | },
15 | "include": ["src/**/*.ts"],
16 | "exclude": ["node_modules", "build"]
17 | }
18 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "mcp-status-observer",
3 | "version": "0.7.0",
4 | "description": "Model Context Protocol Server for monitoring Operational Status of major digital platforms in Claude Desktop",
5 | "main": "build/index.js",
6 | "type": "module",
7 | "scripts": {
8 | "build": "tsc",
9 | "start": "node build/index.js",
10 | "dev": "tsc --watch"
11 | },
12 | "dependencies": {
13 | "@modelcontextprotocol/sdk": "^1.10.1",
14 | "axios": "^1.9.0",
15 | "cheerio": "^1.0.0"
16 | },
17 | "devDependencies": {
18 | "@types/node": "^22.14.1",
19 | "typescript": "^5.2.2"
20 | }
21 | }
22 |
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
1 | name: mcp-status-observer
2 | displayName: Status Observer MCP
3 | description: Monitor the operational status of major digital platforms in Claude Desktop using a Model Context Protocol Server.
4 | visibility: public
5 | type: mcp
6 | author:
7 | name: Iván Luna
8 | url: https://github.com/imprvhub
9 | repository: https://github.com/imprvhub/mcp-status-observer
10 | keywords:
11 | - status
12 | - monitoring
13 | - platforms
14 | - operational
15 | files:
16 | - README.md
17 | - package.json
18 | - tsconfig.json
19 | - Dockerfile
20 | - src/index.ts
21 | startCommand:
22 | type: stdio
23 | configSchema:
24 | type: object
25 | properties: {}
26 | commandFunction: |-
27 | (config) => ({
28 | command: 'node',
29 | args: ['build/index.js'],
30 | env: {}
31 | })
32 | exampleConfig: {}
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
2 | # Build stage
3 | FROM node:lts-alpine AS build
4 | WORKDIR /app
5 |
6 | # Copy dependency manifests and TypeScript config
7 | COPY package.json package-lock.json tsconfig.json ./
8 |
9 | # Copy TypeScript source files and public assets
10 | COPY src ./src
11 | COPY public ./public
12 |
13 | # Install dependencies and build
14 | RUN npm install
15 | RUN npm run build
16 |
17 | # Runtime stage
18 | FROM node:lts-alpine AS runtime
19 | WORKDIR /app
20 |
21 | # Copy built artifacts and production modules
22 | COPY --from=build /app/build ./build
23 | COPY --from=build /app/node_modules ./node_modules
24 | COPY --from=build /app/public ./public
25 |
26 | # Expose port for MCP server if needed
27 | EXPOSE 8888
28 |
29 | # Default command to start the MCP server
30 | CMD ["node", "build/index.js"]
```
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
```yaml
1 | # These are supported funding model platforms
2 |
3 | github: imprvhub
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: ivanlunadev
14 | thanks_dev: # Replace with a single thanks.dev username
15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
16 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | import axios from 'axios';
2 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4 | import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
5 |
6 | interface PlatformStatus {
7 | name: string;
8 | url: string;
9 | description: string;
10 | components?: {
11 | [key: string]: {
12 | name: string;
13 | status: string;
14 | description?: string;
15 | };
16 | };
17 | }
18 |
19 | interface StatusResponse {
20 | status: string;
21 | updated: string;
22 | components?: {
23 | [key: string]: {
24 | name: string;
25 | status: string;
26 | description?: string;
27 | };
28 | };
29 | }
30 |
31 | interface AnthropicStatusResponse {
32 | overall: string;
33 | lastUpdated: string;
34 | services: Array<{
35 | name: string;
36 | status: string;
37 | statusClass: string;
38 | uptime?: number;
39 | }>;
40 | incidents?: Array<{
41 | date: string;
42 | title: string;
43 | impact?: string;
44 | updates?: string[];
45 | }>;
46 | error?: string;
47 | }
48 |
49 | interface AtlassianStatusResponse {
50 | overall: string;
51 | lastUpdated: string;
52 | services: Array<{
53 | name: string;
54 | status: string;
55 | statusClass: string;
56 | url: string;
57 | }>;
58 | error?: string;
59 | }
60 |
61 | interface DockerStatusResponse {
62 | overall: string;
63 | lastUpdated: string;
64 | services: Array<{
65 | name: string;
66 | status: string;
67 | statusClass: string;
68 | description?: string;
69 | }>;
70 | incidents?: Array<any>;
71 | error?: string;
72 | }
73 |
74 | interface GCPStatusResponse {
75 | overall: string;
76 | lastUpdated: string;
77 | services: Array<{
78 | name: string;
79 | status: string;
80 | statusClass: string;
81 | regions?: {
82 | [key: string]: string;
83 | };
84 | }>;
85 | incidents?: Array<{
86 | title: string;
87 | products: string;
88 | locations: string;
89 | updates: string[];
90 | }>;
91 | error?: string;
92 | }
93 |
94 | interface GeminiStatusResponse {
95 | overall: string;
96 | lastUpdated: string;
97 | services: Array<{
98 | name: string;
99 | status: string;
100 | statusClass: string;
101 | }>;
102 | incidents?: Array<{
103 | title: string;
104 | description: string;
105 | status: string;
106 | }>;
107 | error?: string;
108 | }
109 |
110 | interface LinkedInStatusResponse {
111 | overall: string;
112 | lastUpdated: string;
113 | services: Array<{
114 | name: string;
115 | status: string;
116 | statusClass: string;
117 | isChild?: boolean;
118 | }>;
119 | incidents?: Array<{
120 | date: string;
121 | message?: string;
122 | updates?: Array<{
123 | time: string;
124 | title: string;
125 | message: string;
126 | }>;
127 | }>;
128 | error?: string;
129 | }
130 |
131 | interface OpenAIStatusResponse {
132 | overall: string;
133 | lastUpdated: string;
134 | services: Array<{
135 | name: string;
136 | status: string;
137 | statusClass: string;
138 | components?: number;
139 | uptime?: number;
140 | }>;
141 | incidents?: Array<{
142 | title: string;
143 | description: string;
144 | status?: string;
145 | duration?: string;
146 | affects?: string;
147 | statusInfo?: string;
148 | }>;
149 | error?: string;
150 | }
151 |
152 | interface OpenRouterStatusResponse {
153 | overall: string;
154 | lastUpdated: string;
155 | services: Array<{
156 | name: string;
157 | status: string;
158 | statusClass: string;
159 | }>;
160 | incidents?: Array<{
161 | title: string;
162 | description: string;
163 | pubDate: string;
164 | status: string;
165 | impact: string;
166 | isRecent: boolean;
167 | link?: string;
168 | }>;
169 | error?: string;
170 | }
171 |
172 | interface XStatusResponse {
173 | overall: string;
174 | lastUpdated: string;
175 | services: Array<{
176 | name: string;
177 | status: string;
178 | statusClass: string;
179 | }>;
180 | incidents?: Array<{
181 | date: string;
182 | title?: string;
183 | updates?: Array<{
184 | time: string;
185 | message: string;
186 | }>;
187 | }>;
188 | incidentsError?: string;
189 | error?: string;
190 | fallbackError?: string;
191 | }
192 |
193 | interface SupabaseStatusResponse {
194 | overall: string;
195 | lastUpdated: string;
196 | services: Array<{
197 | name: string;
198 | status: string;
199 | statusClass: string;
200 | isGroup?: boolean;
201 | uptime?: string;
202 | children?: Array<{
203 | name: string;
204 | status: string;
205 | statusClass: string;
206 | }>;
207 | }>;
208 | incidents?: Array<{
209 | date: string;
210 | message?: string;
211 | title?: string;
212 | impact?: string;
213 | updates?: Array<{
214 | status?: string;
215 | message?: string;
216 | time?: string;
217 | }>;
218 | }>;
219 | error?: string;
220 | }
221 |
222 | class StatusObserver {
223 | private platforms: Map<string, PlatformStatus>;
224 | private anthropicApiUrl: string;
225 | private atlassianApiUrl: string;
226 | private dockerApiUrl: string;
227 | private geminiApiUrl: string;
228 | private linkedInApiUrl: string;
229 | private openaiApiUrl: string;
230 | private openrouterApiUrl: string;
231 | private supabaseApiUrl: string;
232 | private xApiUrl: string;
233 | private gcpApiUrl: string;
234 |
235 | constructor() {
236 | this.anthropicApiUrl = 'https://status-observer-helpers.vercel.app/anthropic';
237 | this.atlassianApiUrl = 'https://status-observer-helpers.vercel.app/atlassian';
238 | this.dockerApiUrl = 'https://status-observer-helpers.vercel.app/docker';
239 | this.geminiApiUrl = 'https://status-observer-helpers.vercel.app/gemini';
240 | this.linkedInApiUrl = 'https://status-observer-helpers.vercel.app/linkedin';
241 | this.openaiApiUrl = 'https://status-observer-helpers.vercel.app/openai';
242 | this.openrouterApiUrl = 'https://status-observer-helpers.vercel.app/openrouter';
243 | this.supabaseApiUrl = 'https://status-observer-helpers.vercel.app/supabase';
244 | this.xApiUrl = 'https://status-observer-helpers.vercel.app/x';
245 | this.gcpApiUrl = 'https://status-observer-helpers.vercel.app/gcp';
246 | this.platforms = new Map();
247 | this.initializePlatforms();
248 | }
249 |
250 | private initializePlatforms() {
251 | this.addPlatform('amplitude', 'Amplitude', 'https://status.amplitude.com/api/v2/summary.json', 'Analytics platform');
252 | this.addPlatform('anthropic', 'Anthropic', this.anthropicApiUrl, 'AI assistant provider');
253 | this.addPlatform('asana', 'Asana', 'https://status.asana.com/api/v2/summary.json', 'Team workflow management');
254 | this.addPlatform('atlassian', 'Atlassian', this.atlassianApiUrl, 'Developer collaboration tools');
255 | this.addPlatform('cloudflare', 'Cloudflare', 'https://www.cloudflarestatus.com/api/v2/summary.json', 'Web infrastructure and security');
256 | this.addPlatform('digitalocean', 'DigitalOcean', 'https://status.digitalocean.com/api/v2/summary.json', 'Cloud infrastructure');
257 | this.addPlatform('discord', 'Discord', 'https://discordstatus.com/api/v2/summary.json', 'Messaging platform');
258 | this.addPlatform('docker', 'Docker', this.dockerApiUrl, 'Container platform and services');
259 | this.addPlatform('dropbox', 'Dropbox', 'https://status.dropbox.com/api/v2/summary.json', 'File hosting');
260 | this.addPlatform('gcp', 'Google Cloud Platform', this.gcpApiUrl, 'Cloud computing services');
261 | this.addPlatform('gemini', 'Gemini', this.geminiApiUrl, 'Multimodal AI platform');
262 | this.addPlatform('github', 'GitHub', 'https://www.githubstatus.com/api/v2/summary.json', 'Version control platform');
263 | this.addPlatform('linkedin', 'LinkedIn', this.linkedInApiUrl, 'Professional network');
264 | this.addPlatform('netlify', 'Netlify', 'https://www.netlifystatus.com/api/v2/summary.json', 'Web development platform');
265 | this.addPlatform('npm', 'npm', 'https://status.npmjs.org/api/v2/summary.json', 'JavaScript package manager');
266 | this.addPlatform('openai', 'OpenAI', this.openaiApiUrl, 'AI services provider');
267 | this.addPlatform('openrouter', 'OpenRouter', this.openrouterApiUrl, 'AI model routing and access platform');
268 | this.addPlatform('reddit', 'Reddit', 'https://www.redditstatus.com/api/v2/summary.json', 'Social news platform');
269 | this.addPlatform('slack', 'Slack', 'https://status.slack.com/api/v2.0.0/current', 'Business communication');
270 | this.addPlatform('supabase', 'Supabase', this.supabaseApiUrl, 'Open source backend platform');
271 | this.addPlatform('twilio', 'Twilio', 'https://status.twilio.com/api/v2/summary.json', 'Cloud communications');
272 | this.addPlatform('vercel', 'Vercel', 'https://www.vercel-status.com/api/v2/summary.json', 'Frontend deployment platform');
273 | this.addPlatform('x', 'X', this.xApiUrl, 'Social media platform');
274 | }
275 |
276 | private addPlatform(id: string, name: string, url: string, description: string) {
277 | this.platforms.set(id, {
278 | name,
279 | url,
280 | description,
281 | components: {}
282 | });
283 | }
284 |
285 | async getPlatformStatus(platformId: string): Promise<string> {
286 | const platform = this.platforms.get(platformId);
287 | if (!platform) {
288 | return `Platform '${platformId}' not found. Use 'status list' to see available platforms.`;
289 | }
290 |
291 | try {
292 | if (platformId === 'anthropic') {
293 | return await this.getAnthropicStatus(platform);
294 | }
295 |
296 | if (platformId === 'atlassian') {
297 | return await this.getAtlassianStatus(platform);
298 | }
299 |
300 | if (platformId === 'docker') {
301 | return await this.getDockerStatus(platform);
302 | }
303 |
304 | if (platformId === 'gcp') {
305 | return await this.getGCPStatus(platform);
306 | }
307 |
308 | if (platformId === 'gemini') {
309 | return await this.getGeminiStatus(platform);
310 | }
311 |
312 | if (platformId === 'linkedin') {
313 | return await this.getLinkedInStatus(platform);
314 | }
315 |
316 | if (platformId === 'openai') {
317 | return await this.getOpenAIStatus(platform);
318 | }
319 |
320 | if (platformId === 'openrouter') {
321 | return await this.getOpenRouterStatus(platform);
322 | }
323 |
324 | if (platformId === 'supabase') {
325 | return await this.getSupabaseStatus(platform);
326 | }
327 |
328 | if (platformId === 'x') {
329 | return await this.getXStatus(platform);
330 | }
331 |
332 | const response = await axios.get(platform.url);
333 | const data = response.data;
334 |
335 | let statusOutput = `${platform.name} Status:\n`;
336 | statusOutput += `${this.formatOverallStatus(data, platformId)}\n\n`;
337 |
338 | if (data.components && Array.isArray(data.components)) {
339 | statusOutput += `Components:\n`;
340 | data.components.forEach((component: any) => {
341 | statusOutput += `- ${component.name}: ${this.normalizeStatus(component.status)}\n`;
342 | if (component.description) {
343 | statusOutput += ` Description: ${component.description}\n`;
344 | }
345 | });
346 | } else if (data.components && typeof data.components === 'object') {
347 | statusOutput += `Components:\n`;
348 | Object.keys(data.components).forEach(key => {
349 | const component = data.components[key];
350 | statusOutput += `- ${component.name}: ${this.normalizeStatus(component.status)}\n`;
351 | if (component.description) {
352 | statusOutput += ` Description: ${component.description}\n`;
353 | }
354 | });
355 | } else if (platformId === 'github') {
356 | this.processGitHubComponents(data, platform);
357 | statusOutput += this.getGitHubComponentsText(platform);
358 | }
359 |
360 | statusOutput += `\nLast Updated: ${this.formatUpdateTime(data.page?.updated_at || data.updated || new Date().toISOString())}`;
361 |
362 | return statusOutput;
363 | } catch (error) {
364 | console.error(`Error fetching status for ${platform.name}:`, error);
365 |
366 | return `Unable to fetch real-time status for ${platform.name}. The status API might be unavailable or the format has changed.`;
367 | }
368 | }
369 |
370 | private async getOpenRouterStatus(platform: PlatformStatus): Promise<string> {
371 | try {
372 | const response = await axios.get<OpenRouterStatusResponse>(platform.url);
373 | const data = response.data;
374 |
375 | let statusOutput = `${platform.name} Status:\n`;
376 | statusOutput += `Overall: ${this.normalizeStatus(data.overall)}\n\n`;
377 |
378 | if (data.services && data.services.length > 0) {
379 | statusOutput += `Core Components:\n`;
380 | data.services.forEach(service => {
381 | statusOutput += `- ${service.name}: ${this.normalizeStatus(service.status)}\n`;
382 | });
383 | statusOutput += `\n`;
384 | }
385 |
386 | if (data.incidents && data.incidents.length > 0) {
387 | // Filter recent incidents (last 7 days)
388 | const recentIncidents = data.incidents.filter(incident =>
389 | incident.isRecent ||
390 | this.isRecentIncident(incident.pubDate)
391 | );
392 |
393 | const activeIncidents = data.incidents.filter(incident =>
394 | incident.status === 'active' &&
395 | this.isRecentIncident(incident.pubDate)
396 | );
397 |
398 | if (activeIncidents.length > 0) {
399 | statusOutput += `🚨 ACTIVE INCIDENTS:\n`;
400 | activeIncidents.forEach(incident => {
401 | statusOutput += `- ${incident.title}\n`;
402 | statusOutput += ` Impact: ${this.formatImpact(incident.impact)}\n`;
403 | statusOutput += ` Started: ${this.formatDate(incident.pubDate)}\n`;
404 | if (incident.link) {
405 | statusOutput += ` Details: ${incident.link}\n`;
406 | }
407 | statusOutput += `\n`;
408 | });
409 | }
410 |
411 | if (recentIncidents.length > 0 && activeIncidents.length === 0) {
412 | statusOutput += `Recent Resolved Incidents:\n`;
413 | recentIncidents.slice(0, 3).forEach(incident => {
414 | statusOutput += `- ${incident.title} (RESOLVED)\n`;
415 | statusOutput += ` Impact: ${this.formatImpact(incident.impact)}\n`;
416 | statusOutput += ` Date: ${this.formatDate(incident.pubDate)}\n`;
417 | if (incident.description) {
418 | const summary = incident.description.substring(0, 150);
419 | statusOutput += ` Summary: ${summary}${incident.description.length > 150 ? '...' : ''}\n`;
420 | }
421 | if (incident.link) {
422 | statusOutput += ` Details: ${incident.link}\n`;
423 | }
424 | statusOutput += `\n`;
425 | });
426 | }
427 |
428 | if (recentIncidents.length === 0 && data.incidents.length > 0) {
429 | statusOutput += `Recent Activity:\n`;
430 | data.incidents.slice(0, 2).forEach(incident => {
431 | statusOutput += `- ${incident.title}\n`;
432 | statusOutput += ` Impact: ${this.formatImpact(incident.impact)}\n`;
433 | statusOutput += ` Date: ${this.formatDate(incident.pubDate)}\n`;
434 | if (incident.link) {
435 | statusOutput += ` Details: ${incident.link}\n`;
436 | }
437 | statusOutput += `\n`;
438 | });
439 | }
440 | } else {
441 | statusOutput += `No recent incidents reported.\n`;
442 | }
443 |
444 | statusOutput += `Last Updated: ${this.formatUpdateTime(data.lastUpdated || new Date().toISOString())}`;
445 |
446 | return statusOutput;
447 | } catch (error) {
448 | console.error(`Error fetching OpenRouter status:`, error);
449 | return `Unable to fetch real-time status for OpenRouter. The API might be unavailable.`;
450 | }
451 | }
452 |
453 | private isRecentIncident(pubDate: string): boolean {
454 | try {
455 | const incidentDate = new Date(pubDate);
456 | const now = new Date();
457 | const daysDiff = (now.getTime() - incidentDate.getTime()) / (1000 * 3600 * 24);
458 | return daysDiff <= 7; // Consider 7 days as recent
459 | } catch (error) {
460 | return false;
461 | }
462 | }
463 |
464 | private formatImpact(impact: string): string {
465 | switch (impact.toLowerCase()) {
466 | case 'major':
467 | return 'Major Outage 🔴';
468 | case 'degraded':
469 | return 'Degraded Performance ⚠️';
470 | case 'maintenance':
471 | return 'Maintenance 🔧';
472 | case 'minor':
473 | return 'Minor Issue 🟡';
474 | default:
475 | return impact.charAt(0).toUpperCase() + impact.slice(1);
476 | }
477 | }
478 |
479 | private formatDate(dateString: string): string {
480 | try {
481 | const date = new Date(dateString);
482 | return date.toLocaleString('en-US', {
483 | year: 'numeric',
484 | month: 'short',
485 | day: 'numeric',
486 | hour: '2-digit',
487 | minute: '2-digit',
488 | timeZoneName: 'short'
489 | });
490 | } catch (error) {
491 | return dateString;
492 | }
493 | }
494 |
495 | private async getAnthropicStatus(platform: PlatformStatus): Promise<string> {
496 | try {
497 | const response = await axios.get<AnthropicStatusResponse>(platform.url);
498 | const data = response.data;
499 |
500 | let statusOutput = `${platform.name} Status:\n`;
501 | statusOutput += `Overall: ${this.normalizeStatus(data.overall)}\n\n`;
502 |
503 | if (data.services && data.services.length > 0) {
504 | statusOutput += `Components:\n`;
505 | data.services.forEach(service => {
506 | let serviceInfo = `- ${service.name}: ${this.normalizeStatus(service.status)}`;
507 |
508 | if (service.uptime) {
509 | serviceInfo += ` (Uptime: ${service.uptime}%)`;
510 | }
511 |
512 | statusOutput += `${serviceInfo}\n`;
513 | });
514 | } else {
515 | statusOutput += `No component information available.\n`;
516 | }
517 |
518 | if (data.incidents && data.incidents.length > 0) {
519 | statusOutput += `\nRecent Incidents:\n`;
520 | data.incidents.slice(0, 3).forEach(incident => {
521 | statusOutput += `- ${incident.date}: ${incident.title}\n`;
522 |
523 | if (incident.impact) {
524 | statusOutput += ` Impact: ${incident.impact}\n`;
525 | }
526 |
527 | if (incident.updates && incident.updates.length > 0) {
528 | statusOutput += ` Latest update: ${incident.updates[0]}\n`;
529 | }
530 | });
531 | }
532 |
533 | statusOutput += `\nLast Updated: ${this.formatUpdateTime(data.lastUpdated || new Date().toISOString())}`;
534 |
535 | return statusOutput;
536 | } catch (error) {
537 | console.error(`Error fetching Anthropic status:`, error);
538 | return `Unable to fetch real-time status for Anthropic. The API might be unavailable.`;
539 | }
540 | }
541 |
542 | private async getAtlassianStatus(platform: PlatformStatus): Promise<string> {
543 | try {
544 | const response = await axios.get<AtlassianStatusResponse>(platform.url);
545 | const data = response.data;
546 |
547 | let statusOutput = `${platform.name} Status:\n`;
548 | statusOutput += `Overall: ${this.normalizeStatus(data.overall)}\n\n`;
549 |
550 | if (data.services && data.services.length > 0) {
551 | statusOutput += `Components:\n`;
552 | data.services.forEach(service => {
553 | statusOutput += `- ${service.name}: ${this.normalizeStatus(service.status)}\n`;
554 | });
555 | } else {
556 | statusOutput += `No component information available.\n`;
557 | }
558 |
559 | statusOutput += `\nLast Updated: ${this.formatUpdateTime(data.lastUpdated || new Date().toISOString())}`;
560 |
561 | return statusOutput;
562 | } catch (error) {
563 | console.error(`Error fetching Atlassian status:`, error);
564 | return `Unable to fetch real-time status for Atlassian. The API might be unavailable.`;
565 | }
566 | }
567 |
568 | private async getDockerStatus(platform: PlatformStatus): Promise<string> {
569 | try {
570 | const response = await axios.get<DockerStatusResponse>(platform.url);
571 | const data = response.data;
572 |
573 | let statusOutput = `${platform.name} Status:\n`;
574 | statusOutput += `Overall: ${this.normalizeStatus(data.overall)}\n\n`;
575 |
576 | if (data.services && data.services.length > 0) {
577 | statusOutput += `Components:\n`;
578 | data.services.forEach(service => {
579 | statusOutput += `- ${service.name}: ${this.normalizeStatus(service.status)}\n`;
580 | if (service.description) {
581 | statusOutput += ` Description: ${service.description}\n`;
582 | }
583 | });
584 | } else {
585 | statusOutput += `No component information available.\n`;
586 | }
587 |
588 | statusOutput += `\nLast Updated: ${this.formatUpdateTime(data.lastUpdated || new Date().toISOString())}`;
589 |
590 | return statusOutput;
591 | } catch (error) {
592 | console.error(`Error fetching Docker status:`, error);
593 | return `Unable to fetch real-time status for Docker. The API might be unavailable.`;
594 | }
595 | }
596 |
597 | private async getGCPStatus(platform: PlatformStatus): Promise<string> {
598 | try {
599 | const response = await axios.get<GCPStatusResponse>(platform.url);
600 | const data = response.data;
601 |
602 | let statusOutput = `${platform.name} Status:\n`;
603 | statusOutput += `Overall: ${this.normalizeStatus(data.overall)}\n\n`;
604 |
605 | if (data.incidents && data.incidents.length > 0) {
606 | statusOutput += `Active Incidents:\n`;
607 | data.incidents.forEach(incident => {
608 | statusOutput += `- ${incident.title}\n`;
609 | statusOutput += ` Affected Products: ${incident.products}\n`;
610 | statusOutput += ` Affected Locations: ${incident.locations}\n`;
611 | if (incident.updates && incident.updates.length > 0) {
612 | statusOutput += ` Latest Update: ${incident.updates[0]}\n`;
613 | }
614 | });
615 | statusOutput += `\n`;
616 | }
617 |
618 | if (data.services && data.services.length > 0) {
619 | statusOutput += `Service Status by Region:\n\n`;
620 |
621 | const sortedServices = [...data.services].sort((a, b) =>
622 | a.name.localeCompare(b.name)
623 | );
624 |
625 | sortedServices.forEach(service => {
626 | if (service.regions && Object.keys(service.regions).length > 0) {
627 | statusOutput += `${service.name}:\n`;
628 |
629 | Object.entries(service.regions).forEach(([region, status]) => {
630 | if (status) {
631 | const regionName = this.formatRegionName(region);
632 | statusOutput += ` ${regionName}: ${status}\n`;
633 | }
634 | });
635 |
636 | statusOutput += `\n`;
637 | }
638 | });
639 | } else {
640 | statusOutput += `No detailed service information available.\n`;
641 | }
642 |
643 | statusOutput += `Last Updated: ${this.formatUpdateTime(data.lastUpdated || new Date().toISOString())}`;
644 |
645 | return statusOutput;
646 | } catch (error) {
647 | console.error(`Error fetching GCP status:`, error);
648 | return `Unable to fetch real-time status for Google Cloud Platform. The API might be unavailable.`;
649 | }
650 | }
651 |
652 | private formatRegionName(region: string): string {
653 | const nameMap: Record<string, string> = {
654 | americas: 'Americas',
655 | europe: 'Europe',
656 | asiaPacific: 'Asia Pacific',
657 | middleEast: 'Middle East',
658 | africa: 'Africa',
659 | multiRegions: 'Multi-regions',
660 | global: 'Global'
661 | };
662 |
663 | return nameMap[region] || region;
664 | }
665 |
666 | private async getGeminiStatus(platform: PlatformStatus): Promise<string> {
667 | try {
668 | const response = await axios.get<GeminiStatusResponse>(platform.url);
669 | const data = response.data;
670 |
671 | let statusOutput = `${platform.name} Status:\n`;
672 | statusOutput += `Overall: ${this.normalizeStatus(data.overall)}\n\n`;
673 |
674 | if (data.services && data.services.length > 0) {
675 | statusOutput += `Components:\n`;
676 | data.services.forEach(service => {
677 | statusOutput += `- ${service.name}: ${this.normalizeStatus(service.status)}\n`;
678 | });
679 | } else {
680 | statusOutput += `No component information available.\n`;
681 | }
682 |
683 | if (data.incidents && data.incidents.length > 0) {
684 | statusOutput += `\nActive Incidents:\n`;
685 | data.incidents.forEach(incident => {
686 | statusOutput += `- ${incident.title}\n`;
687 | if (incident.description) {
688 | statusOutput += ` Description: ${incident.description}\n`;
689 | }
690 | if (incident.status) {
691 | statusOutput += ` Status: ${incident.status}\n`;
692 | }
693 | });
694 | }
695 |
696 | statusOutput += `\nLast Updated: ${this.formatUpdateTime(data.lastUpdated || new Date().toISOString())}`;
697 |
698 | return statusOutput;
699 | } catch (error) {
700 | console.error(`Error fetching Gemini status:`, error);
701 | return `Unable to fetch real-time status for Gemini. The API might be unavailable.`;
702 | }
703 | }
704 |
705 | private async getLinkedInStatus(platform: PlatformStatus): Promise<string> {
706 | try {
707 | const response = await axios.get<LinkedInStatusResponse>(platform.url);
708 | const data = response.data;
709 |
710 | let statusOutput = `${platform.name} Status:\n`;
711 | statusOutput += `Overall: ${this.normalizeStatus(data.overall)}\n\n`;
712 |
713 | if (data.services && data.services.length > 0) {
714 | const mainServices = data.services.filter(service => !service.isChild);
715 | if (mainServices.length > 0) {
716 | statusOutput += `Components:\n`;
717 | mainServices.forEach(service => {
718 | statusOutput += `- ${service.name}: ${this.normalizeStatus(service.status)}\n`;
719 | });
720 | }
721 |
722 | const childServices = data.services.filter(service => service.isChild);
723 | if (childServices.length > 0) {
724 | statusOutput += `\nSubcomponents:\n`;
725 | childServices.forEach(service => {
726 | statusOutput += ` - ${service.name}: ${this.normalizeStatus(service.status)}\n`;
727 | });
728 | }
729 | } else {
730 | statusOutput += `No component information available.\n`;
731 | }
732 |
733 | statusOutput += `\nLast Updated: ${this.formatUpdateTime(data.lastUpdated || new Date().toISOString())}`;
734 |
735 | return statusOutput;
736 | } catch (error) {
737 | console.error(`Error fetching LinkedIn status:`, error);
738 | return `Unable to fetch real-time status for LinkedIn. The API might be unavailable.`;
739 | }
740 | }
741 |
742 | private async getOpenAIStatus(platform: PlatformStatus): Promise<string> {
743 | try {
744 | const response = await axios.get<OpenAIStatusResponse>(platform.url);
745 | const data = response.data;
746 |
747 | let statusOutput = `${platform.name} Status:\n`;
748 | statusOutput += `Overall: ${this.normalizeStatus(data.overall)}\n\n`;
749 |
750 | if (data.incidents && data.incidents.length > 0) {
751 | statusOutput += `Active Incidents:\n`;
752 | data.incidents.forEach(incident => {
753 | statusOutput += `- ${incident.title}\n`;
754 | if (incident.description) {
755 | statusOutput += ` Description: ${incident.description}\n`;
756 | }
757 | if (incident.affects) {
758 | statusOutput += ` Affects: ${incident.affects}\n`;
759 | }
760 | if (incident.duration) {
761 | statusOutput += ` Duration: ${incident.duration}\n`;
762 | }
763 | if (incident.status) {
764 | statusOutput += ` Status: ${incident.status}\n`;
765 | }
766 | });
767 | statusOutput += `\n`;
768 | }
769 |
770 | if (data.services && data.services.length > 0) {
771 | statusOutput += `Components:\n`;
772 | data.services.forEach(service => {
773 | let serviceInfo = `- ${service.name}: ${this.normalizeStatus(service.status)}`;
774 | if (service.uptime) {
775 | serviceInfo += ` (Uptime: ${service.uptime}%)`;
776 | }
777 |
778 | if (service.components) {
779 | serviceInfo += ` (${service.components} subcomponents)`;
780 | }
781 |
782 | statusOutput += `${serviceInfo}\n`;
783 | });
784 | } else {
785 | statusOutput += `No component information available.\n`;
786 | }
787 |
788 | statusOutput += `\nLast Updated: ${this.formatUpdateTime(data.lastUpdated || new Date().toISOString())}`;
789 |
790 | return statusOutput;
791 | } catch (error) {
792 | console.error(`Error fetching OpenAI status:`, error);
793 | return `Unable to fetch real-time status for OpenAI. The API might be unavailable.`;
794 | }
795 | }
796 |
797 | private async getXStatus(platform: PlatformStatus): Promise<string> {
798 | try {
799 | const response = await axios.get<XStatusResponse>(platform.url);
800 | const data = response.data;
801 |
802 | let statusOutput = `${platform.name} Status:\n`;
803 | statusOutput += `Overall: ${this.normalizeStatus(data.overall)}\n\n`;
804 |
805 | if (data.services && data.services.length > 0) {
806 | statusOutput += `Components:\n`;
807 | data.services.forEach(service => {
808 | statusOutput += `- ${service.name}: ${this.normalizeStatus(service.status)}\n`;
809 | });
810 | } else {
811 | statusOutput += `No component information available.\n`;
812 | }
813 |
814 | if (data.incidents && data.incidents.length > 0) {
815 | statusOutput += `\nRecent Incidents:\n`;
816 | data.incidents.slice(0, 3).forEach(incident => {
817 | statusOutput += `- ${incident.date}: ${incident.title || 'No title'}\n`;
818 |
819 | if (incident.updates && incident.updates.length > 0) {
820 | const latestUpdate = incident.updates[0];
821 | statusOutput += ` Latest update: ${latestUpdate.message || ''}\n`;
822 | if (latestUpdate.time) {
823 | statusOutput += ` Time: ${latestUpdate.time}\n`;
824 | }
825 | }
826 | });
827 | }
828 |
829 | statusOutput += `\nLast Updated: ${this.formatUpdateTime(data.lastUpdated || new Date().toISOString())}`;
830 |
831 | return statusOutput;
832 | } catch (error) {
833 | console.error(`Error fetching X status:`, error);
834 | return `Unable to fetch real-time status for X. The API might be unavailable.`;
835 | }
836 | }
837 |
838 | private async getSupabaseStatus(platform: PlatformStatus): Promise<string> {
839 | try {
840 | const response = await axios.get<SupabaseStatusResponse>(platform.url);
841 | const data = response.data;
842 |
843 | let statusOutput = `${platform.name} Status:\n`;
844 | statusOutput += `Overall: ${this.normalizeStatus(data.overall)}\n\n`;
845 |
846 | if (data.services && data.services.length > 0) {
847 | statusOutput += `Components:\n`;
848 | data.services.forEach(service => {
849 | if (service.isGroup) {
850 | statusOutput += `- ${service.name}: ${this.normalizeStatus(service.status)}\n`;
851 | if (service.children && service.children.length > 0) {
852 | service.children.forEach(child => {
853 | statusOutput += ` - ${child.name}: ${this.normalizeStatus(child.status)}\n`;
854 | });
855 | }
856 | } else {
857 | statusOutput += `- ${service.name}: ${this.normalizeStatus(service.status)}`;
858 | if (service.uptime) {
859 | statusOutput += ` (Uptime: ${service.uptime})`;
860 | }
861 |
862 | statusOutput += `\n`;
863 | }
864 | });
865 | } else {
866 | statusOutput += `No component information available.\n`;
867 | }
868 |
869 | if (data.incidents && data.incidents.length > 0) {
870 | const incidentsWithImpact = data.incidents.filter(inc => inc.impact && inc.impact !== 'none');
871 |
872 | if (incidentsWithImpact.length > 0) {
873 | statusOutput += `\nRecent Incidents:\n`;
874 | incidentsWithImpact.slice(0, 3).forEach(incident => {
875 | statusOutput += `- ${incident.date}: ${incident.title || 'No title'}\n`;
876 | statusOutput += ` Impact: ${incident.impact}\n`;
877 |
878 | if (incident.updates && incident.updates.length > 0) {
879 | const latestUpdate = incident.updates[0];
880 | statusOutput += ` Latest update: ${latestUpdate.status || ''} ${latestUpdate.message || ''}\n`;
881 | if (latestUpdate.time) {
882 | statusOutput += ` Time: ${latestUpdate.time}\n`;
883 | }
884 | }
885 | });
886 | }
887 | }
888 |
889 | statusOutput += `\nLast Updated: ${this.formatUpdateTime(data.lastUpdated || new Date().toISOString())}`;
890 |
891 | return statusOutput;
892 | } catch (error) {
893 | console.error(`Error fetching Supabase status:`, error);
894 | return `Unable to fetch real-time status for Supabase. The API might be unavailable.`;
895 | }
896 | }
897 |
898 | private processGitHubComponents(data: any, platform: PlatformStatus) {
899 | if (data.components && Array.isArray(data.components)) {
900 | platform.components = {};
901 | data.components.forEach((component: any) => {
902 | platform.components[component.id] = {
903 | name: component.name,
904 | status: this.normalizeStatus(component.status),
905 | description: component.description
906 | };
907 | });
908 | }
909 | }
910 |
911 | private getGitHubComponentsText(platform: PlatformStatus): string {
912 | let text = 'Components:\n';
913 |
914 | const githubComponents = [
915 | { id: 'api', name: 'API Requests', description: 'Status for GitHub APIs' },
916 | { id: 'actions', name: 'Actions', description: 'Status of workflows and orchestration for GitHub Actions' },
917 | { id: 'codespaces', name: 'Codespaces', description: 'Status of orchestration and compute for GitHub Codespaces' },
918 | { id: 'copilot', name: 'Copilot', description: 'Status of AI-powered code completion service' },
919 | { id: 'git', name: 'Git Operations', description: 'Performance of git operations (clones, pulls, pushes)' },
920 | { id: 'issues', name: 'Issues', description: 'Status of requests for Issues on GitHub.com' },
921 | { id: 'packages', name: 'Packages', description: 'Status of API requests and webhook delivery for GitHub Packages' },
922 | { id: 'pages', name: 'Pages', description: 'Status of frontend app servers and API for Pages builds' },
923 | { id: 'pulls', name: 'Pull Requests', description: 'Status of requests for Pull Requests on GitHub.com' },
924 | { id: 'webhooks', name: 'Webhooks', description: 'Status of real-time HTTP callbacks' }
925 | ];
926 |
927 | githubComponents.forEach(component => {
928 | const status = platform.components && platform.components[component.id]
929 | ? platform.components[component.id].status
930 | : 'Unknown';
931 |
932 | text += `- ${component.name}: ${status}\n`;
933 | text += ` ${component.description}\n`;
934 | });
935 |
936 | return text;
937 | }
938 |
939 | private formatOverallStatus(data: any, platformId: string): string {
940 | let status = data.status?.description || data.status || 'Unknown';
941 |
942 | if (platformId === 'github') {
943 | status = data.status?.description || (data.status?.indicator === 'none' ? 'operational' : data.status?.indicator) || 'Unknown';
944 | }
945 |
946 | return `Overall: ${this.normalizeStatus(status)}`;
947 | }
948 |
949 | private normalizeStatus(status: string): string {
950 | const lowerStatus = status.toLowerCase();
951 |
952 | if (lowerStatus.includes('operational') || lowerStatus.includes('normal') || lowerStatus === 'good' || lowerStatus === 'ok') {
953 | return 'Operational ✅';
954 | } else if (lowerStatus.includes('degraded') || lowerStatus.includes('partial') || lowerStatus.includes('minor')) {
955 | return 'Degraded Performance ⚠️';
956 | } else if (lowerStatus.includes('major') || lowerStatus.includes('outage') || lowerStatus.includes('down')) {
957 | return 'Major Outage 🔴';
958 | } else if (lowerStatus.includes('maintenance')) {
959 | return 'Under Maintenance 🔧';
960 | } else {
961 | return status.charAt(0).toUpperCase() + status.slice(1);
962 | }
963 | }
964 |
965 | private formatUpdateTime(timestamp: string): string {
966 | try {
967 | const date = new Date(timestamp);
968 | return date.toLocaleString();
969 | } catch (e) {
970 | return timestamp;
971 | }
972 | }
973 |
974 | async getAllPlatformsStatus(): Promise<string> {
975 | let result = 'Status for All Platforms:\n\n';
976 | const platformPromises: Promise<string>[] = [];
977 |
978 | this.platforms.forEach((_, id) => {
979 | platformPromises.push(this.getQuickPlatformStatus(id));
980 | });
981 |
982 | const platformStatuses = await Promise.all(platformPromises);
983 | result += platformStatuses.join('\n\n---\n\n');
984 |
985 | return result;
986 | }
987 |
988 | async getQuickPlatformStatus(platformId: string): Promise<string> {
989 | const platform = this.platforms.get(platformId);
990 | if (!platform) {
991 | return `Platform '${platformId}' not found.`;
992 | }
993 |
994 | try {
995 | if (platformId === 'anthropic') {
996 | const response = await axios.get<AnthropicStatusResponse>(platform.url);
997 | const data = response.data;
998 | return `${platform.name}: ${this.normalizeStatus(data.overall)}`;
999 | }
1000 |
1001 | if (platformId === 'atlassian') {
1002 | const response = await axios.get<AtlassianStatusResponse>(platform.url);
1003 | const data = response.data;
1004 | return `${platform.name}: ${this.normalizeStatus(data.overall)}`;
1005 | }
1006 |
1007 | if (platformId === 'docker') {
1008 | const response = await axios.get<DockerStatusResponse>(platform.url);
1009 | const data = response.data;
1010 | return `${platform.name}: ${this.normalizeStatus(data.overall)}`;
1011 | }
1012 |
1013 | if (platformId === 'gcp') {
1014 | const response = await axios.get<GCPStatusResponse>(platform.url);
1015 | const data = response.data;
1016 | return `${platform.name}: ${this.normalizeStatus(data.overall)}`;
1017 | }
1018 |
1019 | if (platformId === 'linkedin') {
1020 | const response = await axios.get<LinkedInStatusResponse>(platform.url);
1021 | const data = response.data;
1022 | return `${platform.name}: ${this.normalizeStatus(data.overall)}`;
1023 | }
1024 |
1025 | if (platformId === 'gemini') {
1026 | const response = await axios.get<GeminiStatusResponse>(platform.url);
1027 | const data = response.data;
1028 | return `${platform.name}: ${this.normalizeStatus(data.overall)}`;
1029 | }
1030 |
1031 | if (platformId === 'openai') {
1032 | const response = await axios.get<OpenAIStatusResponse>(platform.url);
1033 | const data = response.data;
1034 | return `${platform.name}: ${this.normalizeStatus(data.overall)}`;
1035 | }
1036 |
1037 | if (platformId === 'openrouter') {
1038 | const response = await axios.get<OpenRouterStatusResponse>(platform.url);
1039 | const data = response.data;
1040 | return `${platform.name}: ${this.normalizeStatus(data.overall)}${data.incidents && data.incidents.some(inc => inc.isRecent && inc.status === 'active') ? ' (Active Issues)' : ''}`;
1041 | }
1042 |
1043 | if (platformId === 'supabase') {
1044 | const response = await axios.get<SupabaseStatusResponse>(platform.url);
1045 | const data = response.data;
1046 | return `${platform.name}: ${this.normalizeStatus(data.overall)}`;
1047 | }
1048 |
1049 | if (platformId === 'x') {
1050 | const response = await axios.get<XStatusResponse>(platform.url);
1051 | const data = response.data;
1052 | return `${platform.name}: ${this.normalizeStatus(data.overall)}`;
1053 | }
1054 |
1055 | const response = await axios.get(platform.url);
1056 | const data = response.data;
1057 |
1058 | let status = data.status?.description || data.status || 'Unknown';
1059 | if (platformId === 'github') {
1060 | status = data.status?.description || (data.status?.indicator === 'none' ? 'operational' : data.status?.indicator) || 'Unknown';
1061 | }
1062 |
1063 | return `${platform.name}: ${this.normalizeStatus(status)}`;
1064 | } catch (error) {
1065 | console.error(`Error fetching quick status for ${platform.name}:`, error);
1066 | return `${platform.name}: Unable to fetch status`;
1067 | }
1068 | }
1069 |
1070 | getPlatformsList(): string {
1071 | let result = 'Available Platforms:\n\n';
1072 |
1073 | this.platforms.forEach((platform, id) => {
1074 | result += `- ${platform.name} (use: status --${id})\n`;
1075 | result += ` ${platform.description}\n`;
1076 | });
1077 |
1078 | return result;
1079 | }
1080 | }
1081 |
1082 | const statusObserver = new StatusObserver();
1083 |
1084 | const server = new Server(
1085 | {
1086 | name: "mcp-status-observer",
1087 | version: "0.1.0",
1088 | },
1089 | {
1090 | capabilities: {
1091 | tools: {
1092 | status: {
1093 | description: "Check operational status of major digital platforms including AI providers, cloud services, and developer tools",
1094 | schema: {
1095 | type: "object",
1096 | properties: {
1097 | command: {
1098 | type: "string",
1099 | description: "Command to execute (list, --all, or platform with -- prefix like --openrouter, --openai, --github)"
1100 | }
1101 | },
1102 | required: ["command"]
1103 | }
1104 | }
1105 | },
1106 | },
1107 | }
1108 | );
1109 |
1110 | server.setRequestHandler(ListToolsRequestSchema, async () => {
1111 | return {
1112 | tools: [
1113 | {
1114 | name: "status",
1115 | description: "Check operational status of major digital platforms including AI providers like OpenRouter, OpenAI, Anthropic; cloud services like GCP, Vercel; and developer tools",
1116 | inputSchema: {
1117 | type: "object",
1118 | properties: {
1119 | command: {
1120 | type: "string",
1121 | description: "Command to execute (list, --all, or platform with -- prefix like --openrouter, --openai, --github, --gcp)"
1122 | }
1123 | },
1124 | required: ["command"]
1125 | }
1126 | }
1127 | ]
1128 | };
1129 | });
1130 |
1131 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
1132 | const { name, arguments: args } = request.params;
1133 |
1134 | try {
1135 | if (name === "status") {
1136 | const command = (typeof args?.command === 'string' ? args.command : '').toLowerCase() || '';
1137 |
1138 | if (command === 'list') {
1139 | return {
1140 | content: [
1141 | {
1142 | type: "text",
1143 | text: statusObserver.getPlatformsList()
1144 | }
1145 | ]
1146 | };
1147 | } else if (command === '--all') {
1148 | return {
1149 | content: [
1150 | {
1151 | type: "text",
1152 | text: await statusObserver.getAllPlatformsStatus()
1153 | }
1154 | ]
1155 | };
1156 | } else if (command.startsWith('--')) {
1157 | const platformId = command.slice(2);
1158 | return {
1159 | content: [
1160 | {
1161 | type: "text",
1162 | text: await statusObserver.getPlatformStatus(platformId)
1163 | }
1164 | ]
1165 | };
1166 | } else {
1167 | throw new Error(`Unknown command: ${command}. Available commands: list, --all, or platform with -- prefix like --openrouter, --openai, --github`);
1168 | }
1169 | }
1170 |
1171 | throw new Error(`Unknown tool: ${name}`);
1172 | } catch (error) {
1173 | console.error(`Error handling request:`, error);
1174 | throw error;
1175 | }
1176 | });
1177 |
1178 | async function main() {
1179 | const transport = new StdioServerTransport();
1180 |
1181 | try {
1182 | await server.connect(transport);
1183 | console.error("MCP Status Observer server running on stdio");
1184 | } catch (error) {
1185 | console.error("Error connecting to transport:", error);
1186 | throw error;
1187 | }
1188 | }
1189 |
1190 | main().catch((error) => {
1191 | console.error("Fatal error in main():", error);
1192 | process.exit(1);
1193 | });
```