# psquare [Health: Active]

**Category:** 🎓 Education  
**Repository:** https://github.com/thehesiod/psquare-mcp  
**GitHub Stars:** 14  
**Views:** 2  
**Installs:** 0  
**Upvotes:** 0  
**Directory Page:** https://allmcps.com/mcp/psquare

## Description
MCP server for ParentSquare school-parent communication platform

## Tools
Capabilities this server exposes over MCP:

- **submit_mfa_code** — Submit a 6-digit MFA verification code to complete ParentSquare login.

    When a ParentSquare tool returns an MFA error, a verification code is sent
    to your email. Check your email for the code and use this tool to complete
    authentication.

    Args:
        code: The 6-digit verification code from your email
    
- **list_schools** — List available schools and students in your ParentSquare account.

    Returns JSON with schools, students, and your user ID.
    Use the school_id and student_id values from this output in other tools.
    For school contact info (phone, address), use get_directory(school_id).
    
- **list_school_features** — List features available for a specific school by parsing its sidebar navigation.

    Returns a list of available sections (Feed, Messages, Calendar, Photos, etc.).
    Different schools may have different features enabled.

    Args:
        school_id: School ID (use list_schools to find available IDs)
    
- **get_feeds** — Get recent posts from a school's feed with titles, authors, dates, and summaries.

    Returns a paginated list of posts. Use get_post with the feed_id to see full details.

    Args:
        school_id: School ID
        page: Page number for pagination (default: 1)
    
- **get_post** — Get full details of a specific post including body text, comments, and attachments.

    Image attachments are returned inline so you can see their contents directly.
    PDF attachments have their text extracted and included inline.

    Args:
        feed_id: Post/feed ID (shown as feed_id in get_feeds results)
    
- **list_conversations** — List message conversations for a school. Returns conversation IDs, participants, and previews.

    To read a full conversation, call get_conversation with BOTH the same school_id
    used here AND the chat_id shown in the results.

    Args:
        school_id: School ID (use list_schools to find available IDs)
    
- **get_conversation** — Read a specific message conversation thread with all messages.

    IMPORTANT: Both school_id and chat_id are required.

    Args:
        school_id: School ID (same school_id used in list_conversations)
        chat_id: Conversation/chat ID (from list_conversations results)
    
- **get_calendar_events** — Get calendar events for a school using the ICS calendar export.

    Returns JSON array of events with title, start/end times, location, and description.
    Note: Some schools post monthly calendars as images or PDFs in the feed instead
    of using the ICS calendar. If this returns no events, use get_feeds to browse
    recent posts — look for posts with "calendar" in the title or image/PDF attachments.
    Use get_post to view them (images are returned inline so you can read them).

    Args:
        school_id: School ID
    
- **list_photos** — List photos posted for a school. Returns photo URLs that can be used with download_file.

    Args:
        school_id: School ID
        page: Page number (default: 1)
    
- **list_files** — List files and documents posted for a school. Returns file URLs for download_file.

    Args:
        school_id: School ID
    
- **download_file** — Download a photo, video, or file to local disk from a ParentSquare URL.

    Returns the local file path where the file was saved.
    Use URLs from list_photos, list_files, or get_post attachment results.

    Args:
        url: The file URL (from list_photos, list_files, or get_post results)
        filename: Optional custom filename. If not provided, uses the original filename from the URL.
    
- **get_directory** — List school contact info and staff directory with names, roles, and phone numbers.

    Returns JSON with school details (phone, address) and staff records.
    Use get_staff_member(school_id, user_id) for full staff details including email, photo, and office hours.

    Args:
        school_id: School ID
    
- **get_staff_member** — Get detailed info for a specific staff member including email, photo, and office hours.

    Use get_directory first to find the user_id, then call this for full details.
    Returns structured data plus an inline profile photo when available.

    Args:
        school_id: School ID
        user_id: Staff member's user ID (from get_directory results)
    
- **list_groups** — List groups at a school with active post counts and descriptions.

    To view a group's posts, call get_group_feed with BOTH the same school_id
    used here AND the group_id shown in the results.

    Args:
        school_id: School ID (use list_schools to find available IDs)
    
- **get_group_feed** — Get posts from a specific group's feed.

    IMPORTANT: Both school_id and group_id are required.

    Args:
        school_id: School ID (same school_id used in list_groups)
        group_id: Group ID (from list_groups results)
    
- **get_student_dashboard** — Get student dashboard information including school, grade, classes, and teachers.

    Args:
        student_id: Student ID (use list_schools to see available students)
    
- **list_signups** — List sign-up and RSVP posts for a school (item donations, volunteer slots, event RSVPs).

    Shows each signup post with its title, progress (e.g. "53/103 Items"), and author.
    Use get_post with the feed_id to see full signup details including individual items.

    Args:
        school_id: School ID
        page: Page number for pagination (default: 1)
    
- **list_notices** — List alerts and notices for a school (urgent alerts, secure documents, consent forms).

    Args:
        school_id: School ID
    
- **list_polls** — List polls for a school with questions, options, and vote counts.

    Shows each poll's question, answer options with vote totals, and which
    option is winning.

    Args:
        school_id: School ID
    
- **list_links** — List quick-access links for a school (calendars, menus, forms, etc.).

    Returns link names and URLs (typically Google Drive or external sites).

    Args:
        school_id: School ID
    
- **list_payments** — List payment items for a school with summary stats and item prices.

    Shows upcoming/paid counts, total amount paid, and each payment post
    with its available items and prices.

    Args:
        school_id: School ID
    
- **list_volunteer_hours** — List your logged volunteer hours for a school.

    Shows each logged entry with month, activity type, notes, and hours.

    Args:
        school_id: School ID
    
- **list_forms** — List available forms and permission slips for a school.

    Shows form titles, authors, and dates. These are signable forms that may
    require parent signatures (e.g. field trip permission, driver forms).
    Use get_post with the feed_id to see the full form content.

    Args:
        school_id: School ID
    
- **list_students** — List students on a school's admin roster (id, name, grade, SIS id, guardians).

    Returns the full roster in one call. Use the `id` from results as `student_id`
    in get_student, edit_student, add_parent, and link_guardian_to_student.

    Args:
        school_id: School ID (from list_schools)
        grade: Optional case-insensitive filter on grade name (e.g. "2nd Grade")
        name_contains: Optional case-insensitive substring filter on student name
    
- **list_parents** — List guardians/parents on a school's admin roster (user_id, name, email, phone, linked students).

    Returns the full parent roster in one call. Use the `user_id` from results in
    edit_parent and link_guardian_to_student.

    Args:
        school_id: School ID (from list_schools)
        name_contains: Optional case-insensitive substring filter on guardian name
        student_name_contains: Optional case-insensitive substring filter on the
            guardian's linked student names
    
- **list_grades** — List a school's grades with their grade_id values (needed for add_student).

    Grades are per-school and discovered at runtime. Use the returned `grade_id`
    with add_student / edit_student.

    Args:
        school_id: School ID (from list_schools)
    
- **get_student** — Get admin detail for one student (name, grade, SIS id, linked guardians, classes).

    Args:
        student_id: Student ID (the `id` from list_students; == roster id)
    
- **add_student** — Create a new student on a school's roster. Requires PS_ENABLE_WRITES.

    Use list_grades(school_id) to find the grade_id. The new student's id is not
    returned by ParentSquare — call list_students afterward to retrieve it.

    The roster is read back after the write, so the result says whether the
    student actually exists. If ParentSquare ever returns a 5xx here, do not
    retry on the status code alone: its error page is rendered after the record
    is saved, so the student may well have been created, and there is no API
    route to delete a duplicate.

    Args:
        school_id: School ID
        first_name: Student first name
        last_name: Student last name
        grade_id: Grade ID (from list_grades)
        student_sis_id: Optional SIS/external student ID
    
- **edit_student** — Update an existing student. Only provided fields change. Requires PS_ENABLE_WRITES.

    Current values (name, SIS id, grade) are read from the edit form and
    preserved for any field you don't pass. Class enrollment is never touched —
    use add_class_students / remove_class_students / move_student_to_class.

    Args:
        school_id: School ID
        student_id: Student ID (from list_students)
        first_name: New first name (optional)
        last_name: New last name (optional)
        student_sis_id: New SIS/external ID (optional)
        grade_id: New grade ID (optional; from list_grades)
    
- **add_parent** — Create a guardian/parent and link them to a student. Requires PS_ENABLE_WRITES.

    A parent is always created attached to at least one student. The new user id
    is not returned — call list_students or the parent roster afterward.

    Args:
        school_id: School ID
        student_id: Student ID to attach the guardian to (from list_students)
        first_name: Guardian first name
        last_name: Guardian last name
        email: Guardian email (optional)
        phone: Guardian phone (optional)
    
- **edit_parent** — Update a guardian's name, email, or phone. Requires PS_ENABLE_WRITES.

    Only provided fields change; existing student links are preserved. Get the
    user_id from list_parents(school_id).

    Args:
        school_id: School ID
        user_id: Guardian's user ID (from list_parents)
        first_name: New first name (optional)
        last_name: New last name (optional)
        email: New email (optional)
        phone: New phone (optional)
    
- **link_guardian_to_student** — Link an existing guardian to an additional student. Requires PS_ENABLE_WRITES.

    Existing guardian-student links are left untouched. (Unlinking is a
    destructive op deferred to the ParentSquare website in v1.)

    Args:
        school_id: School ID
        user_id: Guardian's user ID (from list_parents)
        student_id: Student ID to link (from list_students)
    
- **invite_parent** — Send (or resend) a ParentSquare registration invitation to one guardian. Requires PS_ENABLE_WRITES.

    ParentSquare emails the guardian an invite to activate their account. The
    same endpoint is used to resend to an already-invited (but not yet
    registered) guardian. Get the user_id from list_parents(school_id); only
    guardians with registered=false need inviting.

    Args:
        school_id: School ID
        user_id: Guardian's user ID (from list_parents)
    
- **bulk_invite_parents** — Send registration invitations to multiple guardians at once. Requires PS_ENABLE_WRITES.

    ParentSquare sends each unregistered guardian an email/text invite and
    automatically skips any that are already registered (the returned message
    reports how many of the selected users were actually notified). Get user_ids
    from list_parents(school_id) — typically those with registered=false. To
    "invite all", pass every unregistered user_id from list_parents.

    Args:
        school_id: School ID
        user_ids: Guardian user IDs to invite (from list_parents)
    
- **list_classes** — List a school's classes (sections) with grades, teachers, and room-parent counts.

    Returns every class in one call. Use the `section_id` from results with
    get_class, edit_class, add_class_staff, remove_class_staff, and
    set_class_visibility.

    Args:
        school_id: School ID (from list_schools)
        name_contains: Optional case-insensitive substring filter on class name
        grade: Optional case-insensitive filter on grade name (e.g. "Kindergarten")
    
- **get_class** — Get one class with its full staff list (teachers, assistants, room parents).

    Each staff entry includes the `user_id` (the person) and `assoc_id` (their
    link to this class). Use this before changing staff to see the current state.

    Args:
        section_id: Class/section ID (from list_classes)
    
- **list_staff** — List a school's staff and admins (user_id, name, email, phone, role/title).

    Use the returned `user_id` with add_class_staff to assign a teacher to a class.

    Args:
        school_id: School ID (from list_schools)
        name_contains: Optional case-insensitive substring filter on staff name
    
- **add_class** — Create a new class (section) at a school. Requires PS_ENABLE_WRITES.

    New classes are created **hidden** — they are not visible to staff, parents,
    or students until you call set_class_visibility. Assign teachers afterwards
    with add_class_staff.

    Args:
        school_id: School ID
        name: Class name (e.g. "Ms. Smith's Class")
        grade_ids: One or more grade IDs the class belongs to (from list_grades)
    
- **edit_class** — Rename a class or change its room code / grades. Requires PS_ENABLE_WRITES.

    Only provided fields change — current values are read back from the class
    first and resent, because the underlying endpoint replaces what it is given.
    Staff are not affected; use add_class_staff / remove_class_staff for those.

    Args:
        section_id: Class/section ID (from list_classes)
        school_id: School ID the class belongs to
        name: New class name (optional)
        room: New room code / external ID, e.g. "Room 5" (optional)
        grade_ids: New list of grade IDs (optional; replaces the current grades)
    
- **set_class_visibility** — Show or hide classes for staff, parents, and students. Requires PS_ENABLE_WRITES.

    Newly created classes start hidden, so a class made with add_class needs this
    before anyone can see or post to it. Hidden classes are not visible to staff,
    parents, or students.

    Args:
        school_id: School ID
        section_ids: Class/section IDs to update (from list_classes)
        visible: True to make the classes visible, False to hide them
        date: Optional YYYY-MM-DD date the change takes effect; defaults to today
    
- **add_staff** — Add a teacher or other staff member to a school. Requires PS_ENABLE_WRITES.

    ParentSquare emails the new account an address-verification/registration
    invite. Passing section_ids assigns them to those classes right away (as
    class_role, default TEACHER) — verified live, because ParentSquare's own
    "invited classes" field does not create the assignment until the person
    registers, so this tool makes the assignment itself.

    Args:
        school_id: School ID
        first_name: Staff member's first name
        last_name: Staff member's last name
        email: Email address (optional but needed for them to register)
        phone: Phone number (optional)
        title: Title shown at the school, e.g. "3rd Grade Teacher" (optional)
        staff_id: The school's own staff/external ID (optional)
        is_admin: True to grant school Admin rather than Staff access
        section_ids: Class/section IDs to assign them to (from list_classes)
        class_role: Role for those classes — TEACHER (default) or ASSISTANT
    
- **edit_staff** — Update a staff member's name, email, phone, title, or staff ID. Requires PS_ENABLE_WRITES.

    Only provided fields change; everything else — including their class
    assignments and STAFF/ADMIN access level — is left untouched. Get the
    user_id from list_staff(school_id). To change which classes they teach, use
    add_class_staff / remove_class_staff instead. Guardians are rejected: use
    edit_parent for those.

    Args:
        school_id: School ID
        user_id: Staff member's user ID (from list_staff)
        first_name: New first name (optional)
        last_name: New last name (optional)
        email: New email (optional)
        phone: New phone (optional)
        title: New title shown at the school, e.g. "3rd Grade Teacher" (optional)
        staff_id: The school's own staff/external ID (optional)
    
- **add_class_staff** — Assign teachers, assistants, or room parents to a class. Requires PS_ENABLE_WRITES.

    SAFETY: Never call this tool or remove_class_staff in parallel, even for
    different sections. ParentSquare replaces the full staff list, and concurrent
    writes have caused unrelated associations to disappear. The server serializes
    calls within this process; still use one call at a time and verify with a fresh
    get_class read before the next class-staff write.

    The class's existing staff are read first and preserved — only the listed
    people are added, all in a single update. Anyone already holding the role is
    skipped; anyone on the class under a different role is moved to this one.
    Room parents are ordinary guardians: get their user_id from list_parents.
    Teachers and assistants come from list_staff.

    Args:
        section_id: Class/section ID (from list_classes)
        user_ids: One or more user IDs (list_staff for staff, list_parents for room parents)
        role: One of TEACHER, ASSISTANT, ROOM_PARENT — applied to everyone in user_ids
        class_title: Optional label shown on the class (defaults to the role name)
    
- **remove_class_staff** — Remove staff or room parents from a class. Requires PS_ENABLE_WRITES.

    SAFETY: Never call this tool or add_class_staff in parallel, even for
    different sections. ParentSquare replaces the full staff list, and concurrent
    writes have caused unrelated associations to disappear. The server serializes
    calls within this process; still use one call at a time and verify with a fresh
    get_class read before the next class-staff write.

    Pass user_ids to remove specific people, role to remove everyone holding that
    role on the class (e.g. role="ROOM_PARENT" clears that class's room parents),
    or both to remove only those people who hold that role. Everyone else on the
    class is preserved. To clear room parents school-wide, call list_classes and
    repeat this for each class with room_parent_count > 0.

    Args:
        section_id: Class/section ID (from list_classes)
        user_ids: One or more people to remove (optional if role is given)
        role: Remove all holders of this role: TEACHER, ASSISTANT, or ROOM_PARENT
    
- **list_class_students** — List the students enrolled in one class.

    Args:
        section_id: Class/section ID (from list_classes)
    
- **add_class_students** — Enroll students in a class. Requires PS_ENABLE_WRITES.

    SAFETY: Never call this tool or another class-staff/student-enrollment write
    in parallel. The server serializes section-membership writes within this
    process; still use one call at a time and verify with a fresh read.

    Adds everyone listed in a single call without disturbing the students
    already in the class or their other classes. Students already enrolled are
    skipped, so re-running is safe.

    To assign a whole grade at the start of the year, call this once per
    classroom with that classroom's students — list_students(grade=...) gives
    the ids.

    Args:
        section_id: Class/section ID (from list_classes)
        student_ids: One or more student IDs (from list_students)
    
- **remove_class_students** — Remove specific students from a class. Requires PS_ENABLE_WRITES.

    SAFETY: Never call this tool or another class-staff/student-enrollment write
    in parallel. The server serializes the complete school-map read and all
    resulting student writes within this process.

    Each student's other classes are read first and preserved — only this class
    is dropped. student_ids is required: this tool will not empty a class.

    Args:
        school_id: School ID the class belongs to
        section_id: Class/section ID (from list_classes)
        student_ids: The students to remove (from list_class_students)
    
- **move_student_to_class** — Switch a student from one class to another. Requires PS_ENABLE_WRITES.

    SAFETY: Never call this tool or another class-staff/student-enrollment write
    in parallel. The student's full class list is replaced, so the server holds
    one global lock across the read, computation, and PUT within this process.

    Applies both halves of the switch in a single request. The student's other
    classes are preserved. Omit from_section_id to add the new class while
    keeping every current one.

    Args:
        school_id: School ID the classes belong to
        student_id: Student ID (from list_students)
        to_section_id: Class to move the student into (from list_classes)
        from_section_id: Class to move them out of (optional)
    

## Claude Desktop Quick Installation
Install path detected from listing signals. Uses `uvx` (confidence: high):

```json
"mcpServers": {
  "psquare": {
    "command": "uvx",
    "args": ["parentsquare-mcp"]
  }
}
```

## Documentation & README

# ParentSquare MCP Server

[![MCP Registry](https://img.shields.io/badge/MCP-Registry-blue)](https://registry.modelcontextprotocol.io) [![PyPI](https://img.shields.io/pypi/v/parentsquare-mcp)](https://pypi.org/project/parentsquare-mcp/)

An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that gives Claude access to [ParentSquare](https://www.parentsquare.com), a school-parent communication platform. Since ParentSquare has no public API, this server scrapes the web interface using saved session cookies.

Covers both the **parent/guardian experience** (feeds, posts, calendars, messages, directories, sign-ups, forms, payments) and **school admin roster management** — reading student and guardian rosters, and creating/editing students and guardians plus sending registration invitations. Admin write tools are **off by default**, gated behind `PS_ENABLE_WRITES`, and every write is recorded to a local audit log.

Available on the [MCP Registry](https://registry.modelcontextprotocol.io) as `io.github.thehesiod/psquare` and on [PyPI](https://pypi.org/project/parentsquare-mcp/) as `parentsquare-mcp`.

## Disclaimer

> **This project is not affiliated with, endorsed by, or sponsored by ParentSquare, Inc.** "ParentSquare" and all related names, logos, and trademarks are the property of ParentSquare, Inc.
>
> This server communicates with ParentSquare's **undocumented internal APIs** (scraping the web UI and calling its non-public `/api/v2/` JSON endpoints) — these are not published, not guaranteed to be stable, and may change or be blocked at any time without notice. Use of those interfaces may violate ParentSquare's Terms of Service; you are responsible for reviewing the ToS and deciding whether your use is acceptable.
>
> **Use at your own risk.** The authors and contributors accept no responsibility for any consequences of using this software, including but not limited to: account suspension or termination, data loss or corruption, missed or incorrect notifications, MFA lockouts, leaked session cookies, IP blocks, or any other direct or indirect damages. No warranty is provided — see [LICENSE](https://github.com/thehesiod/psquare-mcp/blob/HEAD/LICENSE) for the full MIT no-warranty clause.
>
> If ParentSquare publishes an official API, this project should be considered deprecated in favor of that.

## Features

### Feed & Posts
- **`get_feeds`** — Browse paginated school feed with titles, authors, summaries, and attachment names
- **`get_post`** — Full post details with body text, comments, poll results, signup items, and **inline image/PDF content** (Claude can "see" attached calendars, flyers, etc.)
- **`get_group_feed`** — Posts from a specific group

### Calendar
- **`get_calendar_events`** — Events from ICS calendar as structured JSON (title, start/end, location, description)
- Falls back to guiding Claude to search feed posts for image/PDF calendars when ICS is empty

### Communication
- **`list_conversations`** / **`get_conversation`** — Read message threads
- **`get_directory`** — Staff directory as structured JSON (name, role, phone, user_id)
- **`get_staff_member`** — Full staff details with email, office hours, and **inline profile photo**

### Media & Files
- **`list_photos`** — Photo gallery with URLs
- **`list_files`** — Document files
- **`download_file`** — Download any attachment to local disk

### Participate
- **`list_signups`** — Sign-up and RSVP posts with progress tracking (e.g. "53/103 Items")
- **`list_notices`** — Alerts and secure documents
- **`list_polls`** — Polls with vote counts and winning options
- **`list_forms`** — Permission slips and signable forms
- **`list_payments`** — Payment items with prices and summary stats
- **`list_volunteer_hours`** — Logged volunteer hours with totals

### Groups & Discovery
- **`list_schools`** — Schools and students as structured JSON
- **`list_school_features`** — Available sections per school (parsed from sidebar)
- **`list_groups`** — Groups with active post counts and descriptions
- **`list_group_members`** — Complete group directory as structured JSON, distinguishing ordinary members, guests, owners, and managers
- **`list_links`** — Quick-access links (Google Drive, external sites)

### Student
- **`get_student_dashboard`** — School, grade, classes, and teachers as structured JSON

### Admin
Read tools are always available; the tools marked *(write)* below are **disabled by default** and only run when `PS_ENABLE_WRITES` is set (see [Enabling admin write tools](#enabling-admin-write-tools)). Every write *attempt*, including one blocked by the gate, is recorded to a local audit log. **No tool deletes a record** — students, guardians, classes, and staff can be created and edited but never deleted, and the tools that remove something only unlink a relationship (a staff assignment, class enrollment, or group membership), leaving the underlying people, classes, and groups intact. Deletion is deliberately left to the ParentSquare website.

#### Roster: students & guardians
- **`list_students`** — School roster (id, name, grade, SIS id, guardians) as structured JSON, with optional `grade` / `name_contains` filters
- **`list_parents`** — Guardian roster (user_id, name, email, phone, linked students) as structured JSON, with optional `name_contains` / `student_name_contains` filters; provides the `user_id` needed by `edit_parent` / `link_guardian_to_student`
- **`list_grades`** — A school's grades and their `grade_id` values (needed for add/edit)
- **`get_student`** — Admin detail for one student (name, grade, SIS id, linked guardians, classes)
- **`add_student`** *(write)* — Create a student in a grade
- **`edit_student`** *(write)* — Update a student's name, SIS id, or grade (unchanged fields preserved)
- **`add_parent`** *(write)* — Create a guardian linked to a student
- **`edit_parent`** *(write)* — Update a guardian's name, email, or phone (existing links preserved)
- **`link_guardian_to_student`** *(write)* — Link an existing guardian to an additional student
- **`invite_parent`** *(write)* — Send (or resend) a ParentSquare registration invitation to one guardian
- **`bulk_invite_parents`** *(write)* — Invite many guardians at once; already-registered guardians are skipped automatically

#### Classes, staff & enrollment
- **`list_classes`** / **`get_class`** — A school's classes, and one class with its full staff list (teachers, assistants, room parents)
- **`add_class`** *(write)* — Create a class; new classes start **hidden** until `set_class_visibility`
- **`edit_class`** *(write)* — Rename a class or change its grades
- **`set_class_visibility`** *(write)* — Show or hide classes (date-driven, defaults to today)
- **`list_staff`** — Staff and admin roster (user_id, name, email, phone, role/title) as structured JSON, with an optional `name_contains` filter; provides the `user_id` needed by `edit_staff` / `add_class_staff`
- **`add_staff`** *(write)* — Add a teacher, staff member, or admin, optionally assigning them to classes
- **`edit_staff`** *(write)* — Update a staff member's name, email, phone, title, or staff ID (class assignments and STAFF/ADMIN access preserved; guardians are rejected — use `edit_parent`)
- **`add_class_staff`** / **`remove_class_staff`** *(write)* — Assign or unassign teachers, assistants, and room parents for a class. Section-membership writes are serialized inside one server process; never issue them in parallel, and verify each result with a fresh read.
- **`list_class_students`** — The students enrolled in a class
- **`add_class_students`** / **`remove_class_students`** / **`move_student_to_class`** *(write)* — Manage which students are enrolled in which classes. These share the same global serialization lock as class-staff writes because student-section updates can replace a student's full enrollment list.

#### Group membership
- **`add_group_members`** *(write)* — Add existing people to a manually selected group (`group_id`, `user_ids`). Preserves saved user/student selections, owners, managers, and existing guests; skips people already present. Does not create accounts or invite new guests.
- **`remove_group_members`** *(write)* — Remove explicit ordinary/guest members, one verified operation per person. Refuses empty targets, owners/managers, and groups with student-derived membership; skips absent people. Stops on an uncertain result and reports previously verified changes.

Group writes currently support manually selected (`CsvGroup`) groups, not rule-based
groups or ownership changes. They share the class/enrollment write lock, validate
complete directory reads, and verify both membership and saved selections after
each write. Use them serially, even across server processes. Classroom Room Parent
roles and membership of a Room Parents group are independent.

### Authentication
- **`submit_mfa_code`** — Complete MFA verification with a 6-digit code
- Supports MCP elicitation for inline MFA prompts (set `PS_NO_ELICIT` to disable for unattended callers)
- Session cookies persisted to `~/.parentsquare_cookies.json`
- Credentials loaded from environment variables, 1Password, or LastPass CLI on session expiry

## Setup

### Enabling admin write tools

The admin write tools — every tool marked *(write)* under [Admin](#admin), covering
the student/guardian roster, classes, staff, class enrollment, and groups — modify live
school data, so they are **off by default**. To enable them, set `PS_ENABLE_WRITES=1`
(or `true`/`yes`/`on`) in the server's environment and restart. Every write attempt
(including blocked ones) is appended as JSONL to `PS_AUDIT_LOG` (default
`~/.parentsquare_audit.log`). The admin read tools work regardless.

ParentSquare's form endpoints answer every accepted POST with the same generic
`200` "reload" response, which some silent failures also return. To avoid
false-positive successes, the create/link tools (`add_student`, `add_parent`,
`link_guardian_to_student`) **read back authoritative state after the write** and
only report `✅ Success (verified)` once the new record is actually found. If the
POST is accepted but the read-back can't find the change, they return a `⚠️`
warning that it likely did not persist; if the read-back itself can't run, they
report the write as submitted-but-unverified.

The read-back also overrules a `5xx`. ParentSquare renders its error page *after*
the transaction commits, so a server error can hide a write that actually landed
— `add_student` did exactly that on every create until a missing
`student[section_ids][]` form param was tracked down. Reporting those as failures
invited retries, and each retry duplicated a real student with no API route to
delete one. So when a write returns a `5xx` but the record is found on read-back,
the tool reports `✅ Success (verified)` with a note not to retry. An explicit
rejection (a `4xx`, or a `200` carrying an `alert-danger` flash) is still
reported as a failure regardless of read-back.

### Prerequisites

Credentials can be provided in either of two ways (checked in this order):

1. **Environment variables** — set `PS_USERNAME` and `PS_PASSWORD`
2. **A credential manager** selected by `PS_CREDENTIAL_PROVIDER` (default `1password`):
   - **[1Password CLI](https://developer.1password.com/docs/cli/)** (`op`) — with a "Parentsquare" item containing `username` and `password` fields
   - **[LastPass CLI](https://github.com/LastPass/lastpass-cli)** (`lpass`) — set `PS_CREDENTIAL_PROVIDER=lastpass`. Run `lpass login <your-lastpass-email>` in a terminal first (may prompt for MFA). The item read defaults to `parentsquare.com` and can be overridden with `PS_LASTPASS_ITEM` (an exact entry name or entry ID).

### Install in Claude Code

```bash
claude mcp add --transport stdio parentsquare -- uvx --from "parentsquare-mcp @ git+https://github.com/thehesiod/psquare-mcp" parentsquare-mcp
```

To enable PDF text extraction for post attachments (optional, AGPL-3.0 licensed):

```bash
claude mcp add --transport stdio parentsquare -- uvx --from "parentsquare-mcp[pdf] @ git+https://github.com/thehesiod/psquare-mcp" parentsquare-mcp
```

### That's It

No further configuration needed. The server **auto-discovers** your schools, students, and user ID from ParentSquare on first use. Authentication is handled automatically — when the session expires, the server loads your credentials from environment variables (or 1Password CLI) and re-authenticates (including MFA if needed).

To use environment variables with Claude Code, add an `env` block to your MCP config:

```json
{
  "mcpServers": {
    "parentsquare": {
      "command": "uvx",
      "args": ["parentsquare-mcp"],
      "env": {
        "PS_USERNAME": "your@email.com",
        "PS_PASSWORD": "your-password"
      }
    }
  }
}
```

> **Security note:** environment variables place your password in plaintext inside your MCP config file. If you chose a password manager specifically to avoid that, prefer the 1Password or LastPass CLI path.

To use the LastPass CLI instead of 1Password, log in once (`lpass login <your-lastpass-email>`) and set the provider in your MCP config:

```json
{
  "mcpServers": {
    "parentsquare": {
      "command": "uvx",
      "args": ["parentsquare-mcp"],
      "env": {
        "PS_CREDENTIAL_PROVIDER": "lastpass",
        "PS_LASTPASS_ITEM": "parentsquare.com"
      }
    }
  }
}
```

`PS_LASTPASS_ITEM` is optional (defaults to `parentsquare.com`).

### Unattended use (`PS_NO_ELICIT`)

When MFA is required, the server prompts for the code inline via MCP elicitation.
That prompt waits for a human, so an unattended caller — a claude.ai routine, a
scheduled job — has nobody to answer it and the tool call simply blocks until the
elicitation times out.

Set `PS_NO_ELICIT=1` to skip the prompt. The tool returns the "MFA verification
required" message immediately, and the caller can retrieve the code out of band
(via a Gmail or Microsoft 365 MCP, for example) and pass it to `submit_mfa_code`:

```json
{
  "mcpServers": {
    "parentsquare": {
      "command": "uvx",
      "args": ["parentsquare-mcp"],
      "env": {
        "PS_NO_ELICIT": "1"
      }
    }
  }
}
```

The check is presence-based: **any** non-empty value disables elicitation, so
`PS_NO_ELICIT=0` and `PS_NO_ELICIT=false` also disable it. To re-enable inline
prompting, unset the variable entirely.

## How It Works

The server uses `requests` + `BeautifulSoup` to scrape ParentSquare's server-rendered HTML pages. Each tool follows the pattern:

1. **Fetch** the HTML page via `PSClient.get_page()` or JSON via `PSClient.get_json()` (auto-relogins on session expiry)
2. **Parse** with a dedicated parser in `parsers/` that extracts structured data into dataclasses
3. **Return** results as either structured JSON dicts (for data-lookup tools) or markdown text (for content-rich tools)

Data-lookup tools (`list_schools`, `get_directory`, `get_calendar_events`, `get_student_dashboard`, `get_staff_member`) return structured JSON for easy programmatic access. Content tools (`get_post`, `get_feeds`, `get_conversation`) return markdown.

On first use, the server auto-discovers your schools, students, and user ID from ParentSquare (no config file needed).

For `get_post`, image attachments are downloaded and returned as MCP `Image` objects (so Claude can see them), and PDF attachments have their text extracted via pymupdf. `get_staff_member` also returns inline profile photos.

Group discovery uses GraphQL (`/graphql`); group membership uses the HTML directory
and Rails forms. The school directory and staff details use the internal `/api/v2/` JSON:API.

## Dependencies

| Package | Purpose | License |
|---------|---------|---------|
| `mcp` | Model Context Protocol SDK | MIT |
| `requests` | HTTP client | Apache 2.0 |
| `beautifulsoup4` | HTML parsing | MIT |
| `icalendar` | ICS calendar parsing | BSD |
| `pymupdf` | PDF text extraction (optional) | AGPL-3.0 |

## License

MIT — see [LICENSE](https://github.com/thehesiod/psquare-mcp/blob/HEAD/LICENSE). Note: the optional `pymupdf` dependency is AGPL-3.0 licensed.

mcp-name: io.github.thehesiod/psquare

