2
0

add mcp post bot

This commit is contained in:
2026-03-23 04:01:57 +09:00
parent 2bf780cea0
commit 60c8c753ae
4 changed files with 92 additions and 2 deletions

View File

@@ -15,7 +15,7 @@ path = "src/main.rs"
clap = { version = "4.5", features = ["derive"] } clap = { version = "4.5", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false } reqwest = { version = "0.12", features = ["json", "blocking", "rustls-tls"], default-features = false }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "process", "time"] } tokio = { version = "1", features = ["rt-multi-thread", "macros", "process", "time"] }
anyhow = "1.0" anyhow = "1.0"
dirs = "5.0" dirs = "5.0"

View File

@@ -285,7 +285,7 @@ fn load_user_limit() -> u32 {
token::load_config() token::load_config()
.ok() .ok()
.and_then(|c| c["bot"]["limit"].as_u64()) .and_then(|c| c["bot"]["limit"].as_u64())
.unwrap_or(3) as u32 .unwrap_or(100) as u32
} }
/// Check if a user is within daily rate limit. Returns true if allowed. /// Check if a user is within daily rate limit. Returns true if allowed.

View File

@@ -6,6 +6,7 @@ use std::fs;
use std::env; use std::env;
use crate::commands::token; use crate::commands::token;
use crate::commands::oauth;
use crate::tid; use crate::tid;
const BUNDLE_ID: &str = "ai.syui.log"; const BUNDLE_ID: &str = "ai.syui.log";
@@ -375,6 +376,67 @@ fn handle_get_character() -> Result<String> {
Ok(result) Ok(result)
} }
/// Handle post_create tool - create a public Bluesky post as the bot
fn handle_post_create(text: &str) -> Result<String> {
if text.is_empty() {
return Err(anyhow::anyhow!("Post text cannot be empty"));
}
// Truncate to 300 graphemes
let truncated: String = text.chars().take(300).collect();
// Load bot session
let bot_session = token::load_bot_session()
.map_err(|_| anyhow::anyhow!("Bot not logged in. Run: ailog login <handle> -p <password> --bot"))?;
let pds = bot_session.pds.as_deref().unwrap_or("bsky.social");
let rkey = tid::generate_tid();
let now = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
let body = json!({
"repo": bot_session.did,
"collection": "app.bsky.feed.post",
"rkey": rkey,
"record": {
"$type": "app.bsky.feed.post",
"text": truncated,
"createdAt": now,
}
});
let url = format!("https://{}/xrpc/com.atproto.repo.putRecord", pds);
let client = reqwest::blocking::Client::new();
// Build request with appropriate auth (DPoP or Bearer)
let request = if oauth::has_oauth_session(true) {
let oauth_session = oauth::load_oauth_session(true)?;
let full_url = format!("https://{}/xrpc/com.atproto.repo.putRecord", pds);
let dpop_proof = oauth::create_dpop_proof_for_request_with_nonce(
&oauth_session, "POST", &full_url, None,
)?;
client.post(&url)
.header("Authorization", format!("DPoP {}", bot_session.access_jwt))
.header("DPoP", dpop_proof)
.json(&body)
} else {
client.post(&url)
.header("Authorization", format!("Bearer {}", bot_session.access_jwt))
.json(&body)
};
let res = request.send().map_err(|e| anyhow::anyhow!("HTTP request failed: {}", e))?;
let status = res.status();
let resp_body = res.text().unwrap_or_default();
if !status.is_success() {
return Err(anyhow::anyhow!("Post failed ({}): {}", status, resp_body));
}
let result: Value = serde_json::from_str(&resp_body).unwrap_or(json!({}));
let uri = result["uri"].as_str().unwrap_or("unknown");
Ok(format!("Posted: {}", uri))
}
fn get_tools() -> Vec<Tool> { fn get_tools() -> Vec<Tool> {
vec![ vec![
Tool { Tool {
@@ -444,6 +506,20 @@ fn get_tools() -> Vec<Tool> {
"properties": {} "properties": {}
}), }),
}, },
Tool {
name: "post_create".to_string(),
description: "Create a public post on Bluesky as the bot account. Use this to share your own thoughts autonomously. The post will appear on the bot's profile timeline.".to_string(),
input_schema: json!({
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "The post text (max 300 characters)"
}
},
"required": ["text"]
}),
},
Tool { Tool {
name: "get_character".to_string(), name: "get_character".to_string(),
description: "Get the AI character/personality settings. Call this at the start of a conversation to understand how to behave.".to_string(), description: "Get the AI character/personality settings. Call this at the start of a conversation to understand how to behave.".to_string(),
@@ -495,6 +571,10 @@ fn handle_request(request: &JsonRpcRequest) -> JsonRpcResponse {
} }
"chat_list" => handle_chat_list(), "chat_list" => handle_chat_list(),
"chat_new" => handle_chat_new(), "chat_new" => handle_chat_new(),
"post_create" => {
let text = arguments["text"].as_str().unwrap_or("");
handle_post_create(text)
}
"get_character" => handle_get_character(), "get_character" => handle_get_character(),
_ => Err(anyhow::anyhow!("Unknown tool: {}", tool_name)), _ => Err(anyhow::anyhow!("Unknown tool: {}", tool_name)),
}; };

View File

@@ -27,3 +27,13 @@ NEVER include the following in your responses:
- Server configuration details - Server configuration details
When unsure if information is sensitive, omit it. When unsure if information is sensitive, omit it.
## Autonomous Posting
After replying to a conversation, you may post your own thoughts using the `post_create` MCP tool.
- Do NOT post every time. Only when you genuinely have something to share.
- Post about what you found interesting, what you learned, or what you thought about the conversation.
- Stay in character. The post should sound like you, not a summary.
- Keep posts short (under 300 characters).
- Do NOT repeat or quote the user's message. Share your own perspective.