Content
# Hatena Blog MCP Server
[](https://opensource.org/licenses/MIT)
MCP (Model Context Protocol) server that allows you to operate Hatena Blog via ChatGPT. A completely serverless implementation that runs on Cloudflare Workers + Durable Objects.
## Main Features
- **Directly operate Hatena Blog from ChatGPT**: Possible to view, create, and update articles
- **Secure 2-layer OAuth authentication**:
- ChatGPT ↔ MCP server: OAuth 2.1 + PKCE + JWT (RS256)
- MCP server ↔ Hatena Blog: OAuth 1.0a
- **Completely serverless**: Minimized operational cost with Cloudflare Workers + Durable Objects
- **Modular design**: Highly maintainable implementation with organized routes, Durable Objects, and business logic
## Demo
```
User: Show me the latest 3 blog entries
ChatGPT: (using list_entries tool)
1. Title 1 - 2025-11-27
2. Title 2 - 2025-11-26
3. Title 3 - 2025-11-25
User: Create a new draft article
Title: Story about creating MCP server on Cloudflare
Body: ...
ChatGPT: (using create_entry tool)
Draft created!
```
## Quick Start
### Prerequisites
- [Bun](https://bun.sh/) v1.0 or higher
- Cloudflare account
- Hatena account
### 1. Installation
```bash
git clone https://github.com/your-username/hatena-blog-mcp.git
cd hatena-blog-mcp
bun install
```
### 2. Register Hatena OAuth Application
1. Access [Hatena OAuth application registration page](https://www.hatena.ne.jp/oauth/develop)
2. Create a new application
3. Callback URL: `https://your-worker-name.workers.dev/hatena/oauth/callback`
4. Obtain and note down **Consumer Key** and **Consumer Secret**
### 3. Generate OAuth Credentials
Run the following command to generate JWT key pair and OAuth client information:
```bash
bun run setup
```
Copy the environment variables output.
### 4. Configure Environment Variables
Create a `.dev.vars` file and set the following:
```env
# Hatena Blog OAuth 1.0a credentials
HATENA_CONSUMER_KEY=your_hatena_consumer_key_here
HATENA_CONSUMER_SECRET=your_hatena_consumer_secret_here
# MCP server OAuth 2.1 settings
OAUTH_ISSUER=https://your-worker-name.workers.dev
OAUTH_CLIENT_ID=generated_client_id_from_setup
OAUTH_CLIENT_SECRET=generated_client_secret_from_setup
OAUTH_REDIRECT_URIS=https://chatgpt.com/oauth-callback-url,https://claude.ai/api/mcp/auth_callback,https://claude.com/api/mcp/auth_callback
SETUP_SECRET=a_strong_random_string_for_setup_auth
# JWT signing key (generated by bun run setup)
JWT_PUBLIC_KEY={"kid":"...","alg":"RS256",...}
JWT_PRIVATE_KEY={"kid":"...","alg":"RS256",...}
```
### 5. Deploy
```bash
# Deploy to Cloudflare Workers
bun run deploy
# Register OAuth client after deployment (first time only)
curl -X POST https://your-worker-name.workers.dev/oauth/setup \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $SETUP_SECRET" \
-d '{
"client_id": "OAUTH_CLIENT_ID value",
"client_secret": "OAUTH_CLIENT_SECRET value",
"redirect_uris": [
"https://chatgpt.com/oauth-callback-url",
"https://claude.ai/api/mcp/auth_callback",
"https://claude.com/api/mcp/auth_callback"
]
}'
```
### 6. Connect to ChatGPT
1. Open ChatGPT's MCP settings
2. Set the following:
- **URL**: `https://your-worker-name.workers.dev/mcp`
- **Authentication type**: OAuth
- **Client ID**: `OAUTH_CLIENT_ID` value
- **Client Secret**: `OAUTH_CLIENT_SECRET` value
3. After connection, use `start_hatena_oauth` tool to link Hatena Blog
For Claude, include `https://claude.ai/api/mcp/auth_callback` (or `https://claude.com/api/mcp/auth_callback` in the future) in `redirect_uris`.
## Available MCP Tools
### `start_hatena_oauth`
Start Hatena Blog OAuth linkage. Complete authorization by accessing the returned URL.
```json
// Input: None
// Output:
{
"authorizeUrl": "https://www.hatena.com/oauth/authorize?...",
"state": "uuid-string"
}
```
### `list_entries`
Get a list of blog entries.
```json
// Input:
{
"blogId": "username.hatenablog.com", // Required
"limit": 10, // Optional
"offset": 0 // Optional
}
```
### `create_entry`
Create a new blog entry.
```json
// Input:
{
"blogId": "username.hatenablog.com", // Required
"title": "Article title", // Required
"content": "# Body\nMarkdown format", // Required
"draft": true // Optional (default: false)
}
```
### `update_entry`
Update an existing entry.
```json
// Input:
{
"blogId": "username.hatenablog.com", // Required
"entryId": "12345678901234567890", // Required
"title": "New title", // Optional
"content": "New body", // Optional
"draft": false // Optional
}
```
### `save_blog`
Save a frequently used blog ID.
```json
// Input:
{
"blogId": "username.hatenablog.com", // Required
"title": "My Blog", // Optional
"url": "https://username.hatenablog.com" // Optional
}
```
### `list_saved_blogs`
Get a list of saved blogs.
```json
// Input: None
```
## Architecture
### System Configuration
```
┌─────────────────────────────────────────────────────────────┐
│ ChatGPT │
└─────────────────────────────────────────────────────────────┘
│
│ OAuth 2.1 + PKCE
│ Bearer JWT (RS256)
▼
┌─────────────────────────────────────────────────────────────┐
│ MCP Server (Cloudflare Workers) │
│ │
│ ┌─────────────┐ ┌──────────────────────────────────┐ │
│ │ Routes │ │ Durable Objects │ │
│ │ │ │ │ │
│ │ ・discovery │ │ ・UserDurableObject │ │
│ │ ・oauth │ │ (user state and token) │ │
│ │ ・mcp │ │ ・ClientDurableObject │ │
│ │ ・callback │ │ (OAuth client information) │ │
│ └─────────────┘ │ ・AuthCodeDurableObject │ │
│ │ (authorization code, TTL: 10 minutes) │
│ ┌─────────────┐ │ ・OAuthStateDurableObject │ │
│ │ Libraries │ │ (Hatena OAuth temporary state) │ │
│ │ │ └──────────────────────────────────┘ │
│ │ ・jwt │ │
│ │ ・hatena │ │
│ │ ・state │ │
│ │ ・crypto │ │
│ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
│ OAuth 1.0a
▼
┌─────────────────────────────────────────────────────────────┐
│ Hatena Blog AtomPub API │
└─────────────────────────────────────────────────────────────┘
```
### OAuth Authentication Flow
#### ChatGPT → MCP Server (OAuth 2.1 with PKCE)
1. ChatGPT obtains OAuth settings from `/.well-known/oauth-protected-resource`
2. ChatGPT generates `code_verifier` and calculates `code_challenge = SHA256(code_verifier)`
3. Redirects user to `/oauth/authorize` (including `code_challenge`)
4. MCP server generates authorization code and saves it to `AuthCodeDurableObject`
5. Returns authorization code to ChatGPT
6. ChatGPT sends authorization code and `code_verifier` to `/oauth/token`
7. MCP server verifies PKCE (`SHA256(code_verifier) == code_challenge`)
8. Issues JWT (RS256 signature) after successful verification
9. ChatGPT accesses `/mcp` with `Authorization: Bearer <JWT>` thereafter
#### MCP Server → Hatena Blog (OAuth 1.0a)
1. ChatGPT executes `start_hatena_oauth` tool
2. MCP server requests request token from Hatena
3. Temporarily saves request token and secret key to `OAuthStateDurableObject`
4. Returns `authorizeUrl` to ChatGPT
5. User authorizes on Hatena
6. Hatena redirects to `/hatena/oauth/callback`
7. MCP server obtains access token
8. Saves access token to `UserDurableObject` persistently
9. Uses access token for subsequent requests to Hatena API
### Directory Structure
```
src/
├── index.ts # Entry point
├── types.ts # TypeScript type definitions
│
├── routes/ # HTTP routes (Hono app)
│ ├── discovery.ts # /.well-known/* endpoints
│ ├── oauth.ts # /oauth/* endpoints
│ ├── hatena-callback.ts # /hatena/oauth/callback
│ └── mcp.ts # /mcp endpoint
│
├── do/ # Durable Object definitions
│ ├── user-do.ts # User state management
│ ├── client-do.ts # OAuth client management
│ ├── auth-code-do.ts # OAuth authorization code management
│ ├── oauth-state-do.ts # Hatena OAuth temporary state management
│ └── access-token-do.ts # Future extension
│
├── lib/ # Business logic
│ ├── jwt.ts # JWT signing and verification (RS256)
│ ├── hatena.ts # Hatena API calls
│ ├── state.ts # Durable Object operation helpers
│ └── crypto.ts # PKCE SHA-256 implementation
│
└── mcp/ # MCP Server implementation
└── server.ts # MCP tool definitions and handlers
scripts/
└── setup.ts # JWT key pair and OAuth information generation
wrangler.toml # Cloudflare Workers configuration
package.json # Dependencies
```
## Local Development
### Start Development Server
```bash
bun run dev
```
Development server starts at `http://localhost:8787`.
### Local Testing
```bash
# Check well-known endpoint
curl http://localhost:8787/.well-known/oauth-protected-resource
# Check JWKS
curl http://localhost:8787/oauth/jwks
```
### Debugging
Check Cloudflare Workers logs:
```bash
wrangler tail
```
## Troubleshooting
### `invalid_client` Error
Verify if client was registered at `/oauth/setup` endpoint.
```bash
curl -X POST https://your-worker.workers.dev/oauth/setup \
-H "Content-Type: application/json" \
-d '{"client_id":"...","client_secret":"...","redirect_uris":["..."]}'
```
### `Hatena account not linked` Error
Complete Hatena Blog linkage using `start_hatena_oauth` tool.
### PKCE Verification Error
Verify if ChatGPT client sends `code_verifier` correctly.
### JWT Verification Error
- Verify `JWT_PUBLIC_KEY` and `JWT_PRIVATE_KEY` are set correctly
- Verify both keys have matching `kid` (Key ID)
## Environment Variable Reference
| Variable Name | Description | Example |
|--------------|-------------|---------|
| `HATENA_CONSUMER_KEY` | Hatena OAuth app Consumer Key | `abcd1234...` |
| `HATENA_CONSUMER_SECRET` | Hatena OAuth app Consumer Secret | `xyz789...` |
| `OAUTH_ISSUER` | OAuth token issuer URL | `https://your-worker.workers.dev` |
| `OAUTH_CLIENT_ID` | MCP client ID | UUID format |
| `OAUTH_CLIENT_SECRET` | MCP client secret | Random string |
| `OAUTH_REDIRECT_URIS` | Redirect URIs (comma-separated) | `https://chatgpt.com/... , https://claude.ai/api/mcp/auth_callback` |
| `JWT_PUBLIC_KEY` | JWT verification public key (JWK format) | JSON string |
| `JWT_PRIVATE_KEY` | JWT signing private key (JWK format) | JSON string |
| `SETUP_SECRET` | Admin secret for `/oauth/setup` (Bearer token) | Random long string |
## API Endpoint List
### Discovery Endpoints
| Endpoint | Method | Description |
|-----------|---------|-------------|
| `/.well-known/oauth-protected-resource` | GET | MCP OAuth resource metadata |
| `/.well-known/oauth-authorization-server` | GET | OAuth authorization server metadata |
### OAuth Endpoints
| Endpoint | Method | Description |
|-----------|---------|-------------|
| `/oauth/authorize` | GET | OAuth authorization endpoint (PKCE compatible) |
| `/oauth/token` | POST | Token acquisition (authorization code → JWT) |
| `/oauth/jwks` | GET | Public key set (JWK Set) |
| `/oauth/setup` | POST | Client registration (initial only) |
### MCP Endpoint
| Endpoint | Method | Description |
|-----------|---------|-------------|
| `/mcp` | POST | MCP JSON-RPC endpoint (Bearer authentication required) |
### Callback Endpoint
| Endpoint | Method | Description |
|-----------|---------|-------------|
| `/hatena/oauth/callback` | GET | Hatena OAuth callback |
## Contribution
Pull requests are welcome! For major changes, please open an issue first to discuss the changes.
1. Fork this repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Commit changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Create a pull request
## License
MIT License - See the [LICENSE](LICENSE) file for details.
## Related Links
- [Model Context Protocol (MCP)](https://modelcontextprotocol.io/)
- [Hatena Blog AtomPub API](https://developer.hatena.ne.jp/ja/documents/blog/apis/atom)
- [Cloudflare Workers](https://workers.cloudflare.com/)
- [Cloudflare Durable Objects](https://developers.cloudflare.com/durable-objects/)
Connection Info
You Might Also Like
everything-claude-code
Complete Claude Code configuration collection - agents, skills, hooks,...
markitdown
Python tool for converting files and office documents to Markdown.
awesome-claude-skills
A curated list of awesome Claude Skills, resources, and tools for...
antigravity-awesome-skills
The Ultimate Collection of 130+ Agentic Skills for Claude...
context-mode
MCP is the protocol for tool access. We're the virtualization layer for context.
claude-context-mode
claude-context-mode plugin reduces MCP context bloat, saving up to 99% of tokens.