loading…
Search for a command to run...
loading…
Provides access to the ProjectSight API for managing construction portfolios, projects, and records like ActionItems, RFIs, and Submittals. It features a modula
Provides access to the ProjectSight API for managing construction portfolios, projects, and records like ActionItems, RFIs, and Submittals. It features a modular architecture supporting CRUD operations, workflow state management, and OAuth2 authentication.
A scalable, maintainable MCP (Model Context Protocol) server that provides access to the ProjectSight API via 425 tools across 48 domains—including portfolios, projects, action items, RFIs, submittals, contracts, budgets, applications for payment, change orders, daily reports, drawings, files, meetings, and more.
This server has been refactored into a modular, teachable structure that demonstrates best practices for building MCP servers. It's designed to be both production-ready and an excellent learning resource.
projectsight tool with intent matching and 35+ named capabilities (multi-tool workflows) defined in mcp/yaml/capabilities.yaml. Set MCP_GATEWAY_ONLY=0 to expose all 425 tools.mcp/yaml/policy.yaml and env overrides (MCP_DELETE_POLICY, MCP_REQUIRE_APPROVAL_FOR_MUTATIONS).projectsight-mcp/
├── mcp/
│ ├── main.py # Entry point (STDIO or --http)
│ ├── config.py # Env config; portfolio/scope resolution
│ ├── request_context.py # Per-request context (actor token, X-* headers)
│ ├── auth.py # OAuth2 client credentials + On-Behalf exchange
│ ├── client.py # ProjectSight API client (retry, token refresh)
│ ├── utils.py # Helpers (e.g. resolve_project)
│ ├── registry.py # Load tool_registry.yaml; HANDLER_REGISTRY
│ ├── policy.py # Delete/mutation policy from policy.yaml + env
│ ├── yaml/
│ │ ├── policy.yaml # delete_policy, mutation_approval
│ │ ├── tool_registry.yaml # Tool metadata for gateway (425 tools)
│ │ └── capabilities.yaml # Named multi-tool workflows (35+ capabilities)
│ ├── scripts/
│ │ └── build_registry.py # Generate tool_registry.yaml from tool modules
│ ├── docs/
│ │ ├── TOOL_STANDARD.md # Docstring and registry standards
│ │ └── TOOL_REGISTRY_MAINTENANCE.md # How to maintain/regenerate registry
│ ├── tools/ # MCP tools organized by domain
│ │ ├── __init__.py # register_tools(); gateway-only vs all-tools
│ │ ├── gateway.py # projectsight(user_request, context, prefer_discovery)
│ │ ├── debug.py # test_connection, debug_token, get_mcp_context_requirements
│ │ ├── projects.py, portfolio.py
│ │ ├── action_items.py, rfis.py, submittals.py, workflow.py, workflow_states.py
│ │ ├── application_for_payment.py, budget.py, budget_code_structure.py, budget_group.py, budget_snapshot.py
│ │ ├── change_order_request.py, potential_co.py, prime_contract_co.py, sub_contract_co.py
│ │ ├── checklist.py, company.py, contact.py, contract.py, contract_invoice.py
│ │ ├── daily_report.py, drawing.py, drawing_set.py, erp_read_only.py
│ │ ├── field_work_directive.py, file.py, folder.py, forecast.py, general_invoice.py
│ │ ├── issue.py, job_costs.py, lookup_list.py, meeting.py, notice.py
│ │ ├── photo.py, po_catalog.py, purchase_order.py, records.py, report_generator.py
│ │ ├── role.py, safety_notice.py, submittal_package.py, transmittal.py, user.py
│ │ └── punch_list.py
│ └── tests/
│ └── test_gateway_intent.py # Registry + intent-matching tests
├── README.md
├── pyproject.toml
└── .env # Create this; not committed
Tool modules register with the MCP instance and with HANDLER_REGISTRY so the gateway can execute them by name.
pip install fastmcp python-dotenv aiohttp uvicorn pyyaml
Or using a virtual environment:
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install fastmcp python-dotenv aiohttp uvicorn pyyaml
(Pyyaml is used by the registry and policy modules.)
Create a .env file in the mcp/ directory (or project root) with your ProjectSight API credentials:
APPLICATION_NAME=your_app_name
CLIENT_ID=your_client_id
CLIENT_SECRET=your_client_secret
PROJECTSIGHT_API_URL=https://cloud.api.trimble.com/projectsight/us1/1.0
PROJECTSIGHT_SCOPE=ProjectSight_-_US1
# Optional: PORTFOLIO_ID=your_portfolio_guid (UUID). If unset, the server discovers a default via GET /accounts and GET /accounts/{accountId}/portfolios at startup.
# Optional: for stage or other envs use TRIMBLE_TOKEN_URL=https://stage.id.trimblecloud.com/oauth/token
Note on PORTFOLIO_ID: PORTFOLIO_ID is optional. If unset, the server calls the ProjectSight API (GET /accounts and GET /accounts/{accountId}/portfolios) at startup and uses the first (or only) portfolio found. You can also discover accounts and portfolios with get_accounts and get_portfolios_for_account, then set PORTFOLIO_ID in .env to a Portfolio GUID (UUID) or pass portfolio_guid on tool calls. The API base URL is unchanged (PROJECTSIGHT_API_URL); all requests, including account/portfolio discovery, use it.
Getting Credentials:
For more information, email [email protected] to request API access.
Note on Scope: The PROJECTSIGHT_SCOPE should match your API subscription region:
ProjectSight_-_US1 for US regionProjectSight_-_EU1 for EU regionProjectSight_-_US2 for Azure US regionRun from the mcp/ directory so that .env and imports resolve correctly (or set PYTHONPATH and ensure .env is loaded from the right place).
STDIO Mode (default for MCP clients):
cd mcp
python main.py
HTTP Streaming Mode:
cd mcp
python main.py --http
With custom host/port:
# Windows PowerShell
$env:MCP_HOST="0.0.0.0"
$env:MCP_PORT="8000"
python main.py --http
# Linux/Mac
export MCP_HOST=0.0.0.0
export MCP_PORT=8000
python main.py --http
Gateway-only mode (default):
The server defaults to exposing only the intelligent projectsight gateway tool so the agent is not overwhelmed by hundreds of tools. No configuration needed. To expose all tools instead (e.g. for debugging), set MCP_GATEWAY_ONLY=0 before starting. See "Intelligent Gateway Tool" below.
Port already in use (10048): If you see "port already in use" or error 10048 when running python main.py --http, use another port: set MCP_PORT=8001 (PowerShell: $env:MCP_PORT="8001"), then start the server again and use http://localhost:8001/mcp in your client. To free port 8000 on Windows: run netstat -ano | findstr :8000, note the PID (last column), then taskkill /PID <pid> /F.
{
"mcpServers": {
"projectsight": {
"command": "python",
"args": ["C:\\Users\\cforey\\Desktop\\projectsight-mcp\\mcp\\main.py"],
"env": {
"APPLICATION_NAME": "your_app_name",
"CLIENT_ID": "your_client_id",
"CLIENT_SECRET": "your_client_secret",
"PORTFOLIO_ID": "your-portfolio-guid",
"MCP_GATEWAY_ONLY": "1"
}
}
}
}
Note: You can also use a .env file instead of setting environment variables in the config. The script will automatically load variables from a .env file in the mcp/ directory. PORTFOLIO_ID can be omitted; the server will discover a default portfolio at startup. MCP_GATEWAY_ONLY=1 is the default (single-tool mode); including it in env makes the behavior explicit for Cursor and other clients.
For MCP clients that support HTTP transport, configure the connection URL:
http://localhost:8000/mcp
When the MCP is used from Trimble Agent Studio (or any client that sends the signed-in user's Trimble ID token), you can configure the MCP with "On behalf of actor token" instead of static credentials. The client sends the user's TID token as Authorization: Bearer <token> on each request. The MCP then exchanges that token for a ProjectSight-scoped token via Trimble Identity's On Behalf / Token Exchange grant and uses it for API calls, so requests run as that user.
Token format: The Bearer token must be a JWT (e.g. Trimble ID token). The MCP detects JWT shape and sends the correct subject_token_type for the exchange. If you see an error like subject_token type 'urn:ietf:params:oauth:token-type:access_token' not supported, ensure the client sends a JWT in Authorization: Bearer, not an opaque access token.
Setup in Agent Studio:
https://your-mcp-host/mcp). The MCP must be reachable over HTTPS when used from Studio.openidProjectSight_-_US1 (US), ProjectSight_-_EU1 (EU), or ProjectSight_-_US2 (Azure US).Confirm exact scope names with Trimble Identity and API Endpoints for your environment (stage vs prod).
Server-side: The MCP still needs CLIENT_ID and CLIENT_SECRET in .env (or environment) to perform the On Behalf token exchange. Set APPLICATION_NAME, PROJECTSIGHT_SCOPE, PORTFOLIO_ID, and PROJECTSIGHT_API_URL as defaults; they can be overridden per request via headers (see below).
Optional per-request headers (when a gateway or client sends them):
X-Portfolio-Id — Portfolio GUID for the request (overrides PORTFOLIO_ID for that request).X-API-Base-URL — ProjectSight API base URL (e.g. for a different region).X-ProjectSight-Scope — Scope used for token exchange (e.g. ProjectSight_-_US1).X-Application-Name — Application name for token/scope.If these headers are not sent, the MCP uses .env defaults and tool parameters (e.g. portfolio_guid) as today.
Fallback: When no Authorization: Bearer token is present (e.g. STDIO or "None" auth in Studio), the MCP uses client credentials from .env as before.
Troubleshooting On Behalf exchange:
| Error | Cause | What to do |
|---|---|---|
JWT error: Signature verification failed |
The Bearer token was not issued by (or cannot be verified by) the same Trimble Identity environment the MCP is using. | Ensure TRIMBLE_TOKEN_URL in .env matches the IdP that issued the token. For stage (e.g. studio.stage.trimble-ai.com) use the stage token URL (e.g. https://stage.id.trimblecloud.com/oauth/token or per Trimble docs); for production use https://id.trimble.com/oauth/token. The client must obtain the token from that same environment. |
Caller is not the intended audience of subject token |
The JWT was issued for a different application; Trimble Identity will not exchange it for a token for this MCP's CLIENT_ID. | Ensure the MCP's CLIENT_ID (in .env) is the application the user signs into, or that the token requested by the client (e.g. Agent Studio) has audience (or resource) that includes this MCP's application. Configure the client/IdP so the subject token's aud includes the MCP's CLIENT_ID. |
subject_token type 'access_token' not supported |
IdP expects a JWT. | The MCP now sends JWT type when the token looks like a JWT. Ensure the client sends a JWT in Authorization: Bearer. |
Automatic fallback when On Behalf fails: If On Behalf token exchange fails (e.g. signature verification or intended audience), the MCP automatically falls back to client credentials so API requests can still be made. Set PORTFOLIO_ID in .env if discovery does not find a portfolio or you need a specific portfolio. Requests then run with the application identity (not the signed-in user).
Workaround when On Behalf is not configured: Omit the Bearer token (use client credentials only), set PORTFOLIO_ID in .env, and the MCP will use the cached client_credentials token for discovery and all tools.
By default (or when MCP_GATEWAY_ONLY=1), the server exposes a single tool projectsight so the agent is not overwhelmed by hundreds of tools. Set MCP_GATEWAY_ONLY=0 to expose all tools.
Named capabilities: The gateway can run named capabilities (e.g. project_overview, contract_and_budget_summary, quality_and_issues) that execute a fixed sequence of tools with shared context. The list is in mcp/yaml/capabilities.yaml.
Parameters:
portfolio_guid, project_id, project_name, contract_id, etc.true, returns only a plan (which tools would run) without executing.Responses:
questions_for_user, then call again with context updated (e.g. user says "Downtown" → send context: { "project_name": "Downtown" }).prefer_discovery=true, returns the steps that would run (no execution).context plus approved: true or confirm_mutation: true to execute.Server policy (no deletes; optional approval for create/update):
action: "policy_blocked" with a message that delete actions require triple check; use the ProjectSight UI or API for deletions.MCP_REQUIRE_APPROVAL_FOR_MUTATIONS=1. Then the first call returns action: "approval_required" with a plan; the agent or user can confirm by calling again with the same context plus approved: true or confirm_mutation: true. Policy is configured in mcp/yaml/policy.yaml and overridden by MCP_DELETE_POLICY and MCP_REQUIRE_APPROVAL_FOR_MUTATIONS in .env.Recommended workflow for the agent:
projectsight(user_request=user_message, context={}).projectsight(user_request=..., context={ ... user answers ... }).prefer_discovery=true) → agent can confirm with user, then call again without prefer_discovery to execute.Context continuity (multi-turn conversations): The gateway is stateless: each call receives only the user_request and context for that call. Successful responses include resolved_context (e.g. project_id, project_name, portfolio_guid, and when relevant record IDs such as rfi_id). Clients should retain this resolved_context and send it as the context argument on the next projectsight call so that follow-up requests (e.g. "change the due date to Feb 19" after listing RFIs) work without the user re-specifying project or RFI. When the user refers to a specific item from a prior result (e.g. "that one", "RFI 005"), the client should add the corresponding ID (e.g. rfi_id: 5) to the context it sends.
No multi-tool sequencing is required by the agent; the gateway resolves project/portfolio context and runs the right internal tools server-side. Tool metadata is in mcp/yaml/tool_registry.yaml; handlers are registered at startup in HANDLER_REGISTRY for execution.
The server provides 425 tools across 48 domains. Full tool list and metadata: mcp/yaml/tool_registry.yaml. Named multi-tool capabilities (e.g. project_overview, contract_and_budget_summary, quality_and_issues): mcp/yaml/capabilities.yaml. When using the gateway, call get_mcp_context_requirements() for required context and tools grouped by domain.
Domains (48): action_items, application_for_payment, budget, budget_code_structure, budget_group, budget_snapshot, change_order_request, checklist, company, contact, contract, contract_invoice, daily_report, debug, drawing, drawing_set, erp_read_only, field_work_directive, file, folder, forecast, general_invoice, issue, job_costs, lookup_list, meeting, notice, photo, po_catalog, portfolio, potential_co, prime_contract_co, projects, punch_list, purchase_order, records, report_generator, rfis, role, safety_notice, sub_contract_co, submittal_package, submittals, transmittal, user, workflow, workflow_states.
Common tools (when not using gateway-only mode): get_projects, list_action_items, list_rfis, list_submittals, test_connection, get_mcp_context_requirements. Use the corresponding list/get tools to discover IDs (e.g. list_contracts, list_budgets) when a tool asks for project_id, contract_id, etc. Portfolio-level tools use portfolio_guid from the parameter or PORTFOLIO_ID from .env (must be a Portfolio GUID/UUID).
This MCP server uses a scalable, maintainable structure with a registry-driven gateway.
main.py loads Config, creates Auth and ProjectSightClient, and calls register_tools(mcp, client). In tools/__init__.py, if MCP_GATEWAY_ONLY=1 (default), internal tool modules register with a no-op MCP so they don't appear as tools to the client, but they still register handlers in HANDLER_REGISTRY. The gateway always registers the single projectsight tool on the real MCP.projectsight(user_request, context?, prefer_discovery?) → keyword-based intent matching against mcp/yaml/tool_registry.yaml and mcp/yaml/capabilities.yaml → resolve portfolio_guid/project_id (and project name) via utils.resolve_project and Config/request context → policy check (delete blocked; mutation approval if required) → execute handler(s) from HANDLER_REGISTRY → return need_more_info | plan | result | policy_blocked | approval_required | error.request_context.py holds per-request actor token and overrides (X-Portfolio-Id, X-API-Base-URL, X-ProjectSight-Scope, X-Application-Name). Auth and Config use these when present (e.g. Agent Studio).policy.py reads mcp/yaml/policy.yaml; env vars MCP_DELETE_POLICY and MCP_REQUIRE_APPROVAL_FOR_MUTATIONS override. Deletes are never run by default; create/update can require context.approved or context.confirm_mutation.flowchart LR
User --> projectsight
projectsight --> IntentMatch
IntentMatch --> ContextResolve
ContextResolve --> PolicyCheck
PolicyCheck --> Handlers
Handlers --> Response
mcp/yaml/tool_registry.yaml (e.g. run python scripts/build_registry.py from mcp/).Gateway intent-matching tests live in mcp/tests/. Run them from the mcp directory:
cd mcp
python -m unittest tests.test_gateway_intent -v
Tests cover registry loading, intent matching for common phrases (e.g. "list submittals", "get projects"), and capability-hint summary.
This structure is designed to be a teaching example - use it as a reference when building your own MCP servers! The code demonstrates:
The server uses OAuth2 client credentials flow with Trimble Identity:
~/.cache/projectsight/token_cache.jsonPortfolio ID: Optional in .env. When unset, the server discovers a default portfolio at startup via the accounts/portfolios API. Use get_accounts and get_portfolios_for_account to list accounts and portfolios, then set PORTFOLIO_ID to a Portfolio GUID (UUID) or pass portfolio_guid to tools. If PORTFOLIO_ID is an integer (Account ID), you must pass portfolio_guid per request or set PORTFOLIO_ID to a UUID.
Server behavior (env): MCP_GATEWAY_ONLY (default 1 = only projectsight tool; 0 = expose all 425 tools). MCP_DELETE_POLICY (default never_run; deletes are never executed). MCP_REQUIRE_APPROVAL_FOR_MUTATIONS (set to 1 to require context.approved or context.confirm_mutation before create/update tools run). Optional: MCP_REQUEST_TIMEOUT_SECONDS, MCP_CLIENT_RETRY_COUNT.
Project Lookup: Many tools support finding projects by name (case-insensitive, partial match) if you don't know the project ID.
RFI Creation: The create_or_update_rfi tool is highly flexible and will:
To make your local server accessible via a public URL, use a tunneling service:
Install ngrok on Windows:
winget install ngrok.ngrok
Start your MCP server:
cd mcp
python main.py --http
Create a tunnel (in a separate terminal):
ngrok http 8000
Use the public URL: ngrok will provide a public URL like https://abc123.ngrok-free.dev.
The MCP endpoint is at /mcp:
https://abc123.ngrok-free.dev/mcp
Install cloudflared: Download from developers.cloudflare.com
Start your MCP server:
cd mcp
python main.py --http
Create a tunnel (in a separate terminal):
cloudflared tunnel --url http://localhost:8000
Use the public URL: Cloudflare will provide a public URL like https://random-subdomain.trycloudflare.com. Your MCP endpoint will be:
https://random-subdomain.trycloudflare.com/mcp
When exposing your server publicly, consider:
Based on updated guidance from Trimble Cloud, you don't need to pass the x-API-key to the trimblepaas.com endpoints. If you do pass it, there is a limit of 50 requests per second.
For detailed API documentation, refer to:
If you were using the old projectsight.py file directly:
python main.py instead of python projectsight.py.env file format (place in mcp/ directory)mcp/main.pyThe old projectsight.py file is still available for reference but the new modular structure is recommended.
mcp/tools/<domain>.py. Each tool module defines register(mcp, handler_registry) and registers both the FastMCP tool and an async handler: handler_registry[name] = my_async_handler. The gateway executes tools by name via get_handler(name) from the registry.mcp/yaml/tool_registry.yaml (name, description, domain, required_context, optional_context, keywords; optionally examples, follow_ups). Run python scripts/build_registry.py from the mcp/ directory to regenerate the registry from docstrings; see TOOL_REGISTRY_MAINTENANCE.md.register(...) calls in mcp/tools/__init__.py. Optional: if the tool is part of a multi-step workflow, add or extend an entry in mcp/yaml/capabilities.yaml.Example pattern:
# mcp/tools/my_domain.py
from fastmcp import FastMCP
_client = None
def register(mcp: FastMCP, handler_registry: dict):
@mcp.tool()
async def my_tool(portfolio_guid: str, param: str) -> dict:
"""One-line summary. Args: portfolio_guid: ... param: ... Returns: ..."""
return await _run_my_tool(portfolio_guid, param)
handler_registry["my_tool"] = _run_my_tool # gateway executes by name
async def _run_my_tool(portfolio_guid: str, param: str) -> dict:
result = await _client.get(f"/{portfolio_guid}/endpoint/{param}")
return {"result": result}
def set_client(client):
global _client
_client = client
This project is provided as-is for use with the ProjectSight API.
Built with best practices in mind - Use this structure as a reference for building your own MCP servers! 🚀
Добавь это в claude_desktop_config.json и перезапусти Claude Desktop.
{
"mcpServers": {
"projectsight-mcp-server": {
"command": "npx",
"args": []
}
}
}