course-mcp
This server allows you to browse and search course content from a configured local directory using four tools:
list-courses: Retrieve all available top-level course directories, giving an overview of which courses are available.list-course-files: List all files directly inside a specified course directory.search-course-file: Perform a case-insensitive literal keyword search within a single UTF-8 text or PDF file. Results include matching line numbers, merged context excerpts, and page numbers for PDFs. Configurable context lines (default 3, max 20) and maximum results (default 20, max 100).search-course: Recursively search all eligible files across an entire course directory (up to depth 5), skipping hidden files, symlinks, and common build/dependency directories.
Note: Scanned PDFs requiring OCR are not supported.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@course-mcplist courses"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
course-mcp
course-mcp is a local Python MCP server for referencing course files,
upcoming work from a UMD Canvas calendar feed, and configured Piazza
discussions.
The project combines safe local-file access with bounded, read-only course data from configured external sources.
Requirements
Python 3.10 through 3.14.
uvfor dependency and environment management.MCP Python SDK 1.x. This release uses the v1 low-level
Serverdecorator API and declaresmcp>=1.28.1,<2. MCP 2 requires an intentional server and test migration; see the official v1-to-v2 migration guide.
Related MCP server: CoursePack Local MCP Server
Current Features
Loads filesystem, calendar, and Piazza configuration lazily from the process environment or, when running from this checkout, its root
.envfile.Restricts file access to paths inside
ROOT_DIR.Provides a
FileServicefor safe file reads.Provides a
CourseServicefor course/file listing and searching.Reads a private Canvas iCalendar feed without requiring a Canvas access token.
Reads configured Piazza discussions through the community-built, unofficial
piazza-apipackage.Exposes MCP tools:
list-courses: lists the top-level course directories underROOT_DIR.list-course-files: lists the direct files inside a course directory.search-course-file: searches one UTF-8 text or text-extractable PDF file within a course using case-insensitive literal matching.search-course: recursively searches eligible files throughout one course.get-upcoming-work: returns assignments and events from a bounded date range in the Canvas calendar feed.list-piazza-courses: lists configured Piazza courses accessible to the authenticated account.list-piazza-posts: returns bounded recent post summaries without loading every full thread.get-piazza-post: returns one bounded normalized Piazza thread.search-piazza-posts: uses Piazza's feed search and returns bounded summaries.
search-course-file requires course_title, a course-relative file_path, and
a non-empty keyword. It optionally accepts context_lines (default 3, maximum
20) and max_results (default 20, maximum 100). Search results are returned as
JSON with matching line numbers and merged context excerpts. PDF results also
identify the one-based page containing each excerpt. Scanned PDFs require OCR
and are not supported.
search-course accepts the same keyword, context_lines, and max_results
search controls, but applies max_results independently to every matching file.
It searches direct course files and directories through depth 5. Hidden entries,
symbolic links, and directories named venv, __pycache__, node_modules,
dist, or build are skipped. Other unreadable or non-searchable files are
also skipped without failing the course-wide search.
Both search tools return schema-validated results in MCP structuredContent.
They also include the same result serialized as JSON TextContent for clients
that do not yet consume structured tool output.
get-upcoming-work accepts optional start_date and end_date values in
YYYY-MM-DD format. Both dates are inclusive; without them, the tool returns
the seven calendar dates beginning today. It also accepts an optional literal
query and max_results from 1 through 100. The result identifies whether its
calendar data is stale, whether it was truncated, and how many calendar events
could not be normalized through skipped_event_count. If a non-empty feed has
no usable events, the tool returns stale cached data when available or a clear
error instead of reporting a fresh empty calendar.
The calendar feed includes dated Canvas assignments and events, but it cannot report submission state, grades, or Canvas To Do items. Course hints and item types remain unknown unless they can be derived reliably from the feed.
Piazza tools are read-only and restricted to the course IDs explicitly listed
in PIAZZA_COURSES. Post text is returned as bounded plain text and identified
as untrusted user-generated content. The tools do not post, answer, edit,
download attachments, expose rosters, or perform instructor operations.
list-piazza-posts accepts a limit from 1 through 25 (default 10) and an
offset from 0 through 500 (default 0). Request additional pages sequentially
only when the previous result has truncated: true. search-piazza-posts
accepts a non-empty query of at most 200 characters and max_results from 1
through 25 (default 10). Piazza results are cached in memory for 60 seconds;
after a refresh failure, an existing cached response may be returned with
stale: true.
The Piazza integration depends on unpublished internal endpoints. It is useful for personal experimentation but is not an official Piazza API, may break when Piazza changes its website, and may be subject to Piazza or institutional usage rules. Keep request limits conservative and do not use it for bulk collection.
Project Layout
src/course_mcp/
server.py MCP server boundary
config/
env.py shared lazy .env loading
filesystem.py course-root configuration
calendar.py Canvas calendar configuration
piazza.py Piazza credentials and course allowlist
mcp_schemas/ MCP JSON Schema contracts
mcp_tools/ MCP tool catalog
models/
calendar_item.py normalized calendar data
course.py course directory data
file.py course file data
piazza.py bounded Piazza domain models
services/
calendar/
feed_client.py bounded private-feed loading and cache metadata
parser.py RFC 5545 parsing
profiler.py aggregate-only feed-shape diagnostics
service.py date filtering and result serialization
factory.py lazy configured calendar construction
course/
service.py course-oriented operations
factory.py lazy course/file composition
file/
service.py safe filesystem access
pdf_extractor.py page-oriented PDF text extraction
factory.py lazy configured file construction
piazza/
client.py timeout-bound adapter around unofficial piazza-api
normalizer.py HTML cleanup and response normalization
profiler.py aggregate-only response-shape diagnostics
service.py allowlisting, limits, caching, and serialization
factory.py lazy configured Piazza construction
tests/
config/ configuration tests
mcp_schemas/ structured-output schema tests
mcp_tools/ tool-catalog tests
models/ model tests
scripts/ development-script tests
server/ MCP registration and dispatch tests
services/
calendar/ calendar client, parser, profiler, and service tests
file/ filesystem and PDF extraction tests
piazza/ Piazza client, normalizer, profiler, and service tests
fixtures/ shared synthetic test data
skills/ project-specific agent skillsConfiguration
Each integration is optional and loaded only when one of its tools is called.
When running from this checkout, create a private .env from the redacted
template:
cp .env.example .env
chmod 600 .envConfigure only the integrations you want to use:
ROOT_DIR="/Users/markseeliger/Desktop/Classes/UMD"
CANVAS_ICAL_URL="https://umd.instructure.com/feeds/calendars/user_REDACTED.ics"
CALENDAR_TIMEZONE="America/New_York"
PIAZZA_EMAIL="student@example.edu"
PIAZZA_PASSWORD="replace-with-your-password"
PIAZZA_COURSES='{"abc123":"CMSC 132","xyz789":"CMSC 216"}'ROOT_DIR must point to an existing directory. Each direct child directory is
treated as a course. It is loaded only when a course-backed tool is called;
importing the server does not require any source configuration.
Process environment variables take precedence over .env. If you install the
wheel outside this checkout, pass configuration through the process environment
because the repository-root .env convention applies only to checkout-based
runs.
The current calendar integration is specific to UMD's ELMS-Canvas host. To
obtain its URL, sign in to ELMS-Canvas, open the global Calendar, select
Calendar Feed in the sidebar, and copy the URL field. The URL is a private
credential: do not commit it, paste it into tickets or chat, or include it in
logs and screenshots. This repository ignores .env.
For offline use, configure a downloaded snapshot instead of the URL:
CANVAS_ICAL_PATH="/absolute/private/path/calendar.ics"
CALENDAR_TIMEZONE="America/New_York"Configure exactly one of CANVAS_ICAL_URL and CANVAS_ICAL_PATH. Calendar
configuration is loaded only when get-upcoming-work is called, so the
existing local course tools remain available without it. Live results are
cached in memory for five minutes; after a refresh failure, a previous result
may be returned with stale: true. CALENDAR_TIMEZONE defaults to
America/New_York.
Piazza configuration is also lazy: the server and unrelated tools work without
Piazza variables. PIAZZA_COURSES is a JSON mapping from Piazza course IDs to
the names the agent should display. A course ID is the value after /class/ in
a Piazza course URL. Every course-scoped call is rejected unless its ID appears
in this mapping, and list-piazza-courses returns only configured courses that
the authenticated account can access.
Store the Piazza password only in a private local environment or MCP process
configuration. Never commit .env, paste credentials into prompts, or include
them in logs. Accounts that require institution-only SSO may not support the
email/password flow used by the unofficial package.
Run Locally
From this project directory:
uv sync --locked
uv run --frozen course-mcpBecause MCP servers run over stdio, they are usually launched by an MCP client rather than run directly by hand.
Install In Codex
Register the server with Codex:
codex mcp add course-mcp \
--env ROOT_DIR=/Users/markseeliger/Desktop/Classes/UMD \
-- uv --directory /Users/markseeliger/Desktop/Coding/create-python-server/course_mcp run --frozen course-mcpThis command passes ROOT_DIR explicitly. When launching from this checkout,
configured Canvas and Piazza tools load their remaining variables from the root
.env. Without those optional values, their tools report configuration errors
while the filesystem tools continue to work.
Verify the registration:
codex mcp get course-mcpOr refresh the registration with the project script:
ROOT_DIR=/Users/markseeliger/Desktop/Classes/UMD ./scripts/update_mcp_server.shIf you change MCP tools, restart Codex or start a new Codex session so the tool list is reloaded.
Development
Inspect the configured Canvas calendar's structure without printing event values or the private feed URL:
uv run --frozen python scripts/inspect_canvas_calendar.pyThe command reports only aggregate counts for usable/skipped events, date and time shapes, selected field presence, and coarse URL types. A zero event count means the feed is valid but does not yet provide representative data for course matching; it is not evidence that the calendar integration is broken.
After knowingly selecting the unofficial Piazza transport, inspect a bounded sample's structure without printing course IDs, post numbers, titles, bodies, names, or cookies:
uv run --frozen python scripts/inspect_piazza_shapes.pyThe inspector loads at most five feed summaries and one full thread from the first configured course. Its output contains aggregate key/type/depth counts only. It still makes live calls to unpublished Piazza endpoints, so do not run it unless that access route is acceptable for your account.
Run the test suite:
uv run --frozen pytest -qRun a compile check:
uv run --frozen python -m compileall -q src/course_mcp tests scriptsVerify the lockfile and package artifacts after dependency or packaging changes:
uv lock --check
uv buildCI also installs the built wheel into a clean Python 3.14 environment. This is important because locked development tests alone do not prove that the wheel's declared dependency ranges resolve to compatible releases.
Debug with MCP Inspector:
npx @modelcontextprotocol/inspector uv --directory /Users/markseeliger/Desktop/Coding/create-python-server/course_mcp run course-mcpAvailable Tools
3 toolslist-course-filesA
List the files in a course. Agents should call list-courses first, then pass one of the returned course titles as course_title.
| Name | Required | Description | Default |
|---|---|---|---|
| course_title | Yes | The course directory title returned by list-courses. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries full behavioral burden. It only states the basic action without disclosing error handling, return format, or any side effects. For a read-only operation, minimal behavior disclosure is insufficient.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two concise sentences: first states purpose, second provides usage guideline. No wasted words, well-structured and front-loaded.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's simplicity (1 parameter, no output schema, no nested objects), the description is mostly complete. It covers the prerequisite workflow, but lacks output description or error behavior, which would improve completeness.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% with parameter description. The description adds value by linking course_title to the output of list-courses, providing essential context for correct parameter usage beyond the schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states 'List the files in a course.' This uses a specific verb ('list') and resource ('files in a course'), and distinguishes from siblings like list-courses (lists courses) and search-course-file (searches files).
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly instructs agents to 'call list-courses first, then pass one of the returned course titles as course_title.' This provides a clear prerequisite and usage workflow, avoiding misuse.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list-coursesA
List the courses the user is currently taking. Agents should use this MCP tool whenever they need to check which courses are available.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The description discloses the core behavior (listing courses) but lacks additional context such as read-only nature, authentication requirements, or performance characteristics. Since no annotations are provided, the description carries full burden and could be more informative.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is two concise sentences, front-loaded with the action. Every sentence earns its place with no wasted words.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given no parameters and a simple purpose, the description is sufficient. It covers the what and when. Could optionally mention the return format, but not critical for this straightforward tool.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool has 0 parameters, so baseline is 4. The description does not need to add parameter details, and it correctly omits any parameter-related information.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description starts with 'List the courses the user is currently taking,' which is a specific verb and resource. It clearly distinguishes from siblings (list-course-files, search-course-file) by focusing on courses rather than files.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
States 'Agents should use this MCP tool whenever they need to check which courses are available,' providing explicit guidance on when to use. No exclusion criteria or alternatives are mentioned, but siblings are clearly different.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search-course-fileA
Search for a literal keyword in one UTF-8 text or PDF file inside a course. Matching is case-insensitive.
| Name | Required | Description | Default |
|---|---|---|---|
| keyword | Yes | The literal text to search for. | |
| file_path | Yes | The path relative to the course directory. | |
| max_results | No | Maximum matching lines to return. | |
| course_title | Yes | The course directory title returned by list-courses. | |
| context_lines | No | Lines of context before and after each match. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden. It discloses case-insensitivity and file type limitations (UTF-8 text or PDF), but does not explain output behavior, error handling, or whether the tool is read-only. Adequate but not comprehensive.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single, well-structured sentence that frontloads the key action and constraints. Every word earns its place with no unnecessary repetition.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given 5 parameters and no output schema, the description covers the core search behavior but lacks details on return format, error cases (e.g., file not found, encoding issues), and pagination. Adequate for a simple search but incomplete for edge scenarios.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the description has minimal added value. It confirms literal keyword matching but does not elaborate on parameter semantics beyond what the schema already provides.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the verb 'Search', the resource 'literal keyword in one UTF-8 text or PDF file inside a course', and specifies case-insensitive matching, making the tool's purpose distinct from sibling list tools.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description implies usage for searching a single file by keyword but does not explicitly state when to use or avoid this tool, nor does it mention alternatives. The context is clear but lacks exclusion criteria.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
3 tool updates
v0.1.0- First observed
list-course-files - First observed
list-courses - First observed
search-course-file
TDQS
Scored across 3 tools
Each tool has a clearly distinct purpose: listing courses, listing files within a course, and searching within a file. There is no overlap or ambiguity.
All tool names follow a consistent verb_noun pattern with lowercase and hyphens (list-courses, list-course-files, search-course-file), making them predictable and easy to understand.
With 3 tools, the set is well-scoped for the server's purpose: listing courses, browsing files, and searching content. It is neither too sparse nor overloaded.
The tools cover the essential operations for a course material retrieval system: discover courses, list files, and search within files. No obvious gaps for the intended read-only use case.
Maintenance
Related MCP Connectors
Read-only MCP server for ClassQuill, a tutoring-business-management platform.
An MCP server for deep research or task groups
MCP server for accessing curated awesome list documentation
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceSafe local MCP server for Windows to list, read, search, patch, backup, and verify code files in allowed folders, with Git integration and dry-run diffs.1MIT
- FlicenseNot gradedqualityCmaintenanceRead-only MCP server that provides Claude Desktop with access to converted Canvas course exports, including Markdown files, reports, and search tools for local course content.-
- FlicenseNot gradedqualityDmaintenanceAn MCP server that provides secure access to local file system operations.-
- FlicenseCqualityCmaintenanceAn MCP server for reading files from the local filesystem. Currently supports listing files in directories on the Desktop.3-