This document consolidates the journey of fixing critical errors in the Substack MCP Plus project, particularly the "'str' object has no attribute 'get'" errors that affected multiple tools.
Initial testing revealed that 5 out of 14 tools were failing with cryptic error messages:
update_post- Not persisting changesget_post_content- Returning empty contentpreview_draft- Not returning URLs- Multiple tools failing with:
'str' object has no attribute 'get'
What we thought: The python-substack library was returning string errors instead of proper exceptions.
What we did:
- Created
api_wrapper.pyto handle string errors - Added defensive programming with type checking
- Wrapped all API calls with error handling
Result: Errors persisted - we were solving the wrong problem.
What we learned: The library uses proper exceptions, not string errors.
What we did:
- Enhanced error handling across all handlers
- Added comprehensive logging
- Created validation methods
Result: Better error messages, but core issues remained.
Discovery: Through testing with a known post (Homer Simpson post ID: 167669176), we found:
- The tool was retrieving data successfully
- But
get_post_contentshowed empty content - Debug revealed:
bodyfield was a JSON string, not a dictionary
The Real Problem: Substack stores post content as JSON strings, not parsed objects.
# What we expected:
post['body'] = {
"type": "doc",
"content": [...]
}
# What we actually got:
post['body'] = '{"type":"doc","content":[...]}' # A string!# In post_handler.py
if isinstance(body, str):
try:
body_data = json.loads(body)
except json.JSONDecodeError:
return body # Return as-is if not JSONThe parsed JSON revealed text was stored in a 'text' field, not 'content':
# Wrong:
text = item.get('content', '')
# Correct:
text = item.get('text', '')Added support for actual Substack JSON structure:
bullet_list(notbulleted-list)list_itemwith nested paragraphsimage2type withattrs.srccaptionedImagewith nested content array
- Always verify assumptions: We assumed the API returned dictionaries, but some fields were JSON strings
- Debug with real data: Using actual post IDs revealed the true data structure
- Read the actual response: The library wasn't broken - we just weren't handling the data correctly
- Substack's content format:
- Published posts:
bodyfield - Draft posts:
draft_bodyfield - Both store content as JSON strings that need parsing
- Published posts:
All tools now working correctly:
- ✅
update_post- Persists changes properly - ✅
get_post_content- Returns full formatted content - ✅
preview_draft- Returns preview URLs - ✅ Content extraction - Handles all Substack content types
- ✅ Error handling - Graceful degradation for unexpected formats
While these errors are fixed, some formatting limitations remain:
- Text formatting (bold/italic) displays as markdown syntax
- Links display as markdown syntax
- Blockquotes show with > prefix
See KNOWN_ISSUES.md for current limitations.