this repo has no description
20
fork

Configure Feed

Select the types of activity you want to include in your feed.

update: improve MCP tool descriptions, add lenient datetime deserialization, fix tile CSP

Enhance atpmcp tool descriptions with actionable language, usage guidance, examples, and clearer error documentation. Add lenient_optional_format serde module to atproto-record for tolerating malformed datetime values during deserialization. Update tile CSP to replace deprecated plugin-types directive with media-src. Add test for malformed createdAt field in calendar events.

+103 -27
+26 -26
crates/atpmcp/src/main.rs
··· 141 141 "tools": [ 142 142 { 143 143 "name": "create_record_cid", 144 - "description": "Compute the DAG-CBOR CID for a JSON record. Accepts a JSON object, serializes it to DAG-CBOR, hashes with SHA-256, and returns the CIDv1 string.", 144 + "description": "Serializes a JSON record to deterministic DAG-CBOR, hashes it with SHA-256, and returns a CIDv1 base32 string. Use this when you need to verify record integrity, compare records, or pre-compute CIDs for AT Protocol operations. Returns an error if the record cannot be serialized to DAG-CBOR.", 145 145 "inputSchema": { 146 146 "type": "object", 147 147 "properties": { 148 148 "record": { 149 149 "type": "object", 150 - "description": "The JSON record object to compute a CID for." 150 + "description": "The JSON record object to compute a CID for (e.g. {\"$type\": \"app.bsky.feed.post\", \"text\": \"Hello\", \"createdAt\": \"2024-01-01T00:00:00.000Z\"})." 151 151 } 152 152 }, 153 153 "required": ["record"], ··· 156 156 }, 157 157 { 158 158 "name": "validate_lexicon_schema", 159 - "description": "Validate a lexicon schema object. Accepts a JSON object representing a lexicon schema and validates its structure, version, NSID, and definitions.", 159 + "description": "Validates an AT Protocol lexicon schema, checking version, NSID, and definition structure. Use this when building or reviewing a Lexicon schema to verify it conforms to the specification before publishing. Returns validation errors describing which fields are invalid or missing.", 160 160 "inputSchema": { 161 161 "type": "object", 162 162 "properties": { 163 163 "schema": { 164 164 "type": "object", 165 - "description": "The lexicon schema to validate." 165 + "description": "The lexicon schema object to validate. Must include 'lexicon' (version integer), 'id' (NSID string), and 'defs' (definitions object)." 166 166 } 167 167 }, 168 168 "required": ["schema"], ··· 171 171 }, 172 172 { 173 173 "name": "resolve_handle_to_did", 174 - "description": "Resolve an AT Protocol handle to its DID. Accepts a handle string (e.g. 'alice.bsky.social') and returns the resolved DID (e.g. 'did:plc:abc123').", 174 + "description": "Resolves an AT Protocol handle (e.g. 'alice.bsky.social') to its DID identifier. Use this when you have a user's handle and need their DID for API calls or record lookups. Returns an error if the handle cannot be resolved via DNS or HTTP.", 175 175 "inputSchema": { 176 176 "type": "object", 177 177 "properties": { 178 178 "handle": { 179 179 "type": "string", 180 - "description": "The AT Protocol handle to resolve." 180 + "description": "The AT Protocol handle to resolve (e.g. 'alice.bsky.social')." 181 181 } 182 182 }, 183 183 "required": ["handle"], ··· 186 186 }, 187 187 { 188 188 "name": "resolve_identity", 189 - "description": "Resolve a DID to its full DID document. Accepts a DID string and returns the complete DID document as JSON.", 189 + "description": "Retrieves the full DID document for a given DID, with optional PLC directory override. Use this when you need to inspect verification methods, service endpoints, or rotation keys for an identity. Returns an error if the DID cannot be resolved or the document is malformed.", 190 190 "inputSchema": { 191 191 "type": "object", 192 192 "properties": { 193 193 "did": { 194 194 "type": "string", 195 - "description": "The DID to resolve." 195 + "description": "The DID to resolve (e.g. 'did:plc:ewvi7nxzyoun6zhxrhs64oiz' or 'did:web:example.com')." 196 196 }, 197 197 "plc_directory_hostname": { 198 198 "type": "string", ··· 205 205 }, 206 206 { 207 207 "name": "parse_facets", 208 - "description": "Parse rich text facets (mentions, URLs, and hashtags) from plain text. Returns AT Protocol facets with correct UTF-8 byte offsets. Mentions are resolved to DIDs when possible.", 208 + "description": "Extracts rich text facets (mentions, URLs, hashtags) from plain text with UTF-8 byte offsets, resolving mentions to DIDs when possible. Use this when composing a Bluesky post that contains mentions, links, or hashtags to generate the required facets array. Returns an empty array if no facets are found; unresolvable mentions are included without a DID.", 209 209 "inputSchema": { 210 210 "type": "object", 211 211 "properties": { 212 212 "text": { 213 213 "type": "string", 214 - "description": "The plain text to parse for facets." 214 + "description": "The plain text to parse for facets (e.g. 'Hello @alice.bsky.social check out https://example.com #atproto')." 215 215 } 216 216 }, 217 217 "required": ["text"], ··· 220 220 }, 221 221 { 222 222 "name": "get_record", 223 - "description": "Retrieve an AT Protocol record by AT-URI. Resolves the identity, finds the PDS endpoint, and retrieves the record using com.atproto.repo.getRecord with unauthenticated access.", 223 + "description": "Retrieves an AT Protocol record by AT-URI, resolving identity and PDS endpoint automatically. Use this when you need to fetch a specific record such as a post, profile, or follow. Returns an error if the AT-URI is malformed, the identity cannot be resolved, or the record does not exist.", 224 224 "inputSchema": { 225 225 "type": "object", 226 226 "properties": { ··· 230 230 }, 231 231 "cid": { 232 232 "type": "string", 233 - "description": "Specific version CID to retrieve." 233 + "description": "Optional CID to retrieve a specific version of the record (e.g. 'bafyreig2...')." 234 234 }, 235 235 "plc_directory_hostname": { 236 236 "type": "string", ··· 243 243 }, 244 244 { 245 245 "name": "get_lexicon", 246 - "description": "Fetch a lexicon schema record by NSID. Resolves the authority from the NSID via DNS if no repository is provided, then retrieves the lexicon from the repository's PDS.", 246 + "description": "Retrieves and describes an AT Protocol lexicon schema by NSID, returning its definitions, required fields, and structure. Use this when you need to inspect the schema for a record type or XRPC method before constructing records or API calls. Returns an error if the NSID authority cannot be resolved or the lexicon record is not found.", 247 247 "inputSchema": { 248 248 "type": "object", 249 249 "properties": { ··· 253 253 }, 254 254 "repo": { 255 255 "type": "string", 256 - "description": "Optional repository DID or handle. If omitted, the authority is resolved from the NSID via DNS." 256 + "description": "Optional repository DID or handle for lexicon resolution (e.g. 'did:plc:ewvi7nxzyoun6zhxrhs64oiz'). If omitted, the authority is resolved from the NSID via DNS." 257 257 } 258 258 }, 259 259 "required": ["nsid"], ··· 262 262 }, 263 263 { 264 264 "name": "validate_xrpc", 265 - "description": "Validate XRPC parameters and input body against a lexicon schema. Fetches the lexicon for the given NSID, determines whether it defines a query or procedure, and validates the provided params and/or body accordingly.", 265 + "description": "Validates XRPC request parameters and body against an AT Protocol lexicon schema, catching missing or invalid input before making a call. Use this before calling invoke_xrpc to verify that your parameters and body conform to the method's lexicon. Returns validation errors listing which fields are invalid or missing.", 266 266 "inputSchema": { 267 267 "type": "object", 268 268 "properties": { ··· 272 272 }, 273 273 "params": { 274 274 "type": "object", 275 - "description": "Query or procedure parameters to validate." 275 + "description": "Query or procedure parameters to validate (e.g. {\"repo\": \"alice.bsky.social\"})." 276 276 }, 277 277 "body": { 278 278 "type": "object", 279 - "description": "Procedure input body to validate." 279 + "description": "Procedure input body to validate as a JSON object." 280 280 }, 281 281 "repo": { 282 282 "type": "string", ··· 289 289 }, 290 290 { 291 291 "name": "invoke_xrpc", 292 - "description": "Make an XRPC request to an AT Protocol service. Supports queries (GET) and procedures (POST). If a JSON body is provided, a POST is made; otherwise GET. Supports unauthenticated, authenticated (via stored atpxrpc credentials), and proxied requests (via the atproto-proxy header through the authenticated user's PDS).", 292 + "description": "Invokes an XRPC method on an AT Protocol service, supporting both queries (GET) and procedures (POST) with optional authentication and proxied requests. Use this when you need to make API calls to a PDS or AppView service. Requires one of endpoint, identity, or auth_handle to determine the target service. Returns the HTTP error response from the service on failure.", 293 293 "inputSchema": { 294 294 "type": "object", 295 295 "properties": { ··· 299 299 }, 300 300 "params": { 301 301 "type": "object", 302 - "description": "Query parameters as key-value string pairs for GET requests.", 302 + "description": "Query parameters as key-value string pairs (e.g. {\"repo\": \"alice.bsky.social\", \"collection\": \"app.bsky.feed.post\"}).", 303 303 "additionalProperties": { "type": "string" } 304 304 }, 305 305 "body": { 306 306 "type": "object", 307 - "description": "JSON body for POST (procedure) requests. If present, the request is a POST." 307 + "description": "JSON body for POST (procedure) requests. If present, the request is sent as POST; otherwise GET." 308 308 }, 309 309 "endpoint": { 310 310 "type": "string", 311 - "description": "Explicit service endpoint URL (e.g. 'https://bsky.network'). Either endpoint, identity, or auth_handle is required." 311 + "description": "Explicit service endpoint URL (e.g. 'https://bsky.network'). One of endpoint, identity, or auth_handle is required." 312 312 }, 313 313 "identity": { 314 314 "type": "string", 315 - "description": "Handle or DID to resolve for PDS endpoint discovery. Either endpoint, identity, or auth_handle is required." 315 + "description": "Handle or DID to resolve for PDS endpoint discovery (e.g. 'alice.bsky.social' or 'did:plc:ewvi7nxzyoun6zhxrhs64oiz'). One of endpoint, identity, or auth_handle is required." 316 316 }, 317 317 "auth_handle": { 318 318 "type": "string", 319 - "description": "Handle of a stored atpxrpc account for authenticated requests. If omitted, the request is unauthenticated." 319 + "description": "Handle of a stored atpxrpc account for authenticated requests (e.g. 'alice.bsky.social'). Configure credentials via the atpxrpc CLI. If omitted, the request is unauthenticated." 320 320 }, 321 321 "proxy": { 322 322 "type": "string", ··· 329 329 }, 330 330 { 331 331 "name": "generate_tid", 332 - "description": "Generate AT Protocol Timestamp Identifiers (TIDs). TIDs are 13-character base32-sortable strings used as record keys. Optionally generate multiple TIDs or generate from a specific microsecond timestamp.", 332 + "description": "Generates AT Protocol Timestamp Identifiers (TIDs). TIDs are 13-character base32-sortable strings used as record keys. Use this when creating new records that require a TID as the record key (rkey). Returns an error if count exceeds 100.", 333 333 "inputSchema": { 334 334 "type": "object", 335 335 "properties": { 336 336 "count": { 337 337 "type": "integer", 338 - "description": "Number of TIDs to generate (default 1, max 100)." 338 + "description": "Number of TIDs to generate (default 1, max 100). Example: 5." 339 339 }, 340 340 "timestamp_micros": { 341 341 "type": "integer", 342 - "description": "Specific microsecond UNIX timestamp to generate from. If omitted, uses current time." 342 + "description": "Specific microsecond UNIX timestamp to generate from (e.g. 1704067200000000 for 2024-01-01T00:00:00Z). If omitted, uses current time." 343 343 } 344 344 }, 345 345 "additionalProperties": false
+1 -1
crates/atproto-dasl/src/tiles.rs
··· 23 23 pub const MAX_NAME_GRAPHEMES: usize = 100; 24 24 25 25 /// Default Content-Security-Policy for tile execution. 26 - pub const DEFAULT_CSP: &str = "default-src 'self' blob: data:; script-src 'self' blob: data: 'unsafe-inline' 'wasm-unsafe-eval'; plugin-types 'none'; manifest-src 'none'; base-uri 'none'"; 26 + pub const DEFAULT_CSP: &str = "default-src 'self' blob: data:; script-src 'self' blob: data: 'unsafe-inline' 'wasm-unsafe-eval'; media-src 'self' blob: data:; manifest-src 'none'; base-uri 'none'"; 27 27 28 28 /// Default sandbox attributes for tile execution. 29 29 pub const SANDBOX_ATTRS: &str =
+42
crates/atproto-record/src/datetime.rs
··· 26 26 //! 27 27 //! - [`format`](crate::datetime::format) - Required datetime field serialization with RFC 3339 millisecond precision 28 28 //! - [`optional_format`](crate::datetime::optional_format) - Optional datetime field serialization with proper None handling 29 + //! - [`lenient_optional_format`](crate::datetime::lenient_optional_format) - Lenient optional datetime deserialization that tolerates malformed values 29 30 30 31 /// Required datetime field serialization with RFC 3339 formatting. 31 32 /// ··· 94 95 .map(Some) 95 96 } 96 97 } 98 + 99 + /// Lenient optional datetime deserialization that tolerates malformed values. 100 + /// 101 + /// This module accepts any JSON value for deserialization. Valid RFC 3339 strings produce 102 + /// `Some(DateTime<Utc>)`, while all other values (objects, nulls, numbers, arrays, booleans, 103 + /// or unparseable strings) produce `None` without error. Serialization matches `optional_format`. 104 + /// 105 + /// Use with `#[serde(default, with = "atproto_record::datetime::lenient_optional_format")]` 106 + /// on `Option<DateTime<Utc>>` fields where the data source may contain garbage values. 107 + pub mod lenient_optional_format { 108 + use chrono::{DateTime, SecondsFormat, Utc}; 109 + use serde::{Deserialize, Deserializer, Serializer}; 110 + 111 + /// Serializes an `Option<DateTime<Utc>>` to RFC 3339 string or null. 112 + pub fn serialize<S>(date: &Option<DateTime<Utc>>, serializer: S) -> Result<S::Ok, S::Error> 113 + where 114 + S: Serializer, 115 + { 116 + match date { 117 + Some(d) => { 118 + let s = d.to_rfc3339_opts(SecondsFormat::Millis, true); 119 + serializer.serialize_str(&s) 120 + } 121 + None => serializer.serialize_none(), 122 + } 123 + } 124 + 125 + /// Deserializes any JSON value to `Option<DateTime<Utc>>`, returning `None` for non-string or unparseable values. 126 + pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error> 127 + where 128 + D: Deserializer<'de>, 129 + { 130 + let value = serde_json::Value::deserialize(deserializer)?; 131 + match value { 132 + serde_json::Value::String(s) => Ok(DateTime::parse_from_rfc3339(&s) 133 + .ok() 134 + .map(|v| v.with_timezone(&Utc))), 135 + _ => Ok(None), 136 + } 137 + } 138 + }
+34
crates/atproto-record/src/lexicon/community_lexicon_calendar_event.rs
··· 712 712 713 713 Ok(()) 714 714 } 715 + 716 + #[test] 717 + fn test_event_with_malformed_created_at() { 718 + let json_str = r#"{ 719 + "mode": "community.lexicon.calendar.event#inperson", 720 + "name": "Lunch at Copelands!", 721 + "uris": [ 722 + { 723 + "uri": "lsdfaopkljdfs/940340cce37871e3f224f.jpg", 724 + "name": "Event Image" 725 + } 726 + ], 727 + "$type": "community.lexicon.calendar.event", 728 + "endsAt": null, 729 + "status": "community.lexicon.calendar.event#scheduled", 730 + "startsAt": "2025-04-12T16:00:00.000Z", 731 + "createdAt": {}, 732 + "locations": [ 733 + { 734 + "lat": 33.87734358679921, 735 + "lon": -84.45593029260637, 736 + "type": "community.lexicon.location.geo", 737 + "description": "Copeland's, 3101, Cobb Parkway South, Riverwood, Atlanta, Cobb County, Georgia, 30339, United States" 738 + } 739 + ], 740 + "description": "Lunch!!&nbsp; first one for 2025" 741 + }"#; 742 + 743 + let result = serde_json::from_str::<TypedEvent>(json_str); 744 + assert!( 745 + result.is_err(), 746 + "Malformed createdAt (empty object) should fail deserialization" 747 + ); 748 + } 715 749 }