custom-mcp-server
Allows sending notifications and updating task status with Slack messages, including posting to channels and threads.
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., "@custom-mcp-serverUpdate annotation task task-123 to completed and notify Slack."
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.
custom-mcp-server
A production Model Context Protocol server for a data-annotation workflow. It exposes six tools over the MCP stdio transport, backed by AWS S3 + DynamoDB and Slack, with JWT auth, per-tool rate limiting, and exponential-backoff retries.
Prerequisites
Node.js 20 LTS
npm
AWS account (S3 bucket + DynamoDB table) and a Slack bot token for runtime use (not required to run the test suite — all external calls are mocked)
Related MCP server: Slack MCP Server
Install
npm installEnvironment setup
Copy .env.example to .env and fill in the values. Keys:
Key | Required by | Notes |
| all AWS tools | e.g. |
| all AWS tools | secret — keep out of source control |
| all AWS tools | secret |
|
| default bucket |
|
| table with partition key |
|
| secret, |
|
| e.g. |
| auth (every call) | expected |
| auth (every call) | expected |
| auth (RS256) | JWKS endpoint for signature verification |
| auth (HS256, dev only) | optional; ≥ 32 chars; refused when |
| auth | set to |
| rate limiter | default |
| retry | default |
| retry | default |
| logger |
|
Build / test / run
npm run build # compile TypeScript to dist/
npm run typecheck # tsc --noEmit
npm test # Jest (ESM) — all external calls mocked
npm start # node dist/server.js (stdio transport)Tools
Tool | Input (required**) | Required scope | Behavior |
|
|
| Upload base64 content to S3 under the caller's prefix; returns |
|
|
| Download object from the caller's prefix; returns |
|
|
| Read a record the caller owns; returns the item (without |
|
|
| Put a record stamped with the caller as |
|
|
| Post to Slack (text sanitized); returns |
|
|
| Read/update a task the caller owns, optionally notify Slack |
Auth model
Every tool call is authenticated and authorized:
Authentication. The caller supplies a JWT via
_meta.authorization(optionallyBearer-prefixed). The expected algorithm is pinned from server configuration — not the token header — to block algorithm-confusion attacks: RS256 (verified againstJWKS_URI) by default, or HS256 only when aJWT_SECRET(≥ 32 chars) is set andNODE_ENVis notproduction. The server checksiss/aud/expiry (with a small clock skew) and derives anAuthContext(subject,scopes). Invalid tokens →AUTH_INVALID.Scope authorization. Each tool declares
requiredScopes. A token missing a required scope is rejected withFORBIDDENbefore the handler runs.Object-level authorization (ownership). DynamoDB records carry an
ownerattribute and S3 keys are confined to a per-subject prefix (<subject>/…). Callers can only read/update their own records and objects; foreign records are reported as not-found to avoid ID enumeration. This prevents IDOR.Error handling. Callers receive only a stable error
codeplus arequestId; full error detail is logged server-side (stderr) and never leaked to the client.
JWKS keys are fetched through a cached, rate-limited client to avoid a network round-trip (and IdP DoS) on every verification. S3 up/downloads are capped at 10 MiB to bound memory use.
Rate limiting & retries
Rate limit: 100 requests/min per principal+tool (configurable), in-memory per process, keyed by
subject:toolso one caller cannot starve others. Unknown tool names are rejected before consuming limiter budget. Exceeding it yieldsRATE_LIMITED.Retry: transient failures (
retryable: true) are retried up to 3 times with exponential backoff (baseDelay * 2^(n-1)). Conflicts, validation, and auth errors are never retried.Idempotency note: retries wrap non-idempotent writes (
dynamo_write,slack_notify). Only transient errors are retried, but adding idempotency keys is recommended future work.
Cursor setup
.cursor/mcp.json registers the server with Cursor:
{
"mcpServers": {
"custom-mcp-server": {
"command": "node",
"args": ["dist/server.js"],
"env": { "AWS_REGION": "us-east-1", "...": "..." }
}
}
}Secrets (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, SLACK_BOT_TOKEN,
JWT_SECRET) are not placed in mcp.json; provide them via your shell
environment / .env. Run npm run build before launching so dist/server.js
exists.
Architecture
See PLAN.md for the full milestone plan, interface contracts, and
blocker analysis. Source layout:
src/
server.ts stdio transport + tool-call pipeline
config.ts env loading + validation (zod)
types.ts shared interface contracts
errors.ts AppError exception + guards
security.ts scopes, ownership, key-scoping & sanitization helpers
logger.ts stderr-only structured logger
auth/oauth.ts JWT validation (algorithm-pinned) + cached JWKS
middleware/ retry.ts, rate-limiter.ts
clients/ s3/dynamo/slack factories
tools/ one file per tool + index.tsAvailable Tools
6 toolsannotation_statusA
Read or update an annotation task's status, optionally notifying Slack.
| Name | Required | Description | Default |
|---|---|---|---|
| taskId | Yes | Annotation task identifier (Dynamo 'id') | |
| newStatus | No | If provided, update the task status | |
| notify | No | If true, post the status to Slack |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description must fully disclose behavior. It indicates read or update, but omits details like required permissions, whether reading requires no updates, side effects of Slack notification (e.g., which channel is used), and if the operation is idempotent.
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?
Single, clear sentence with no unnecessary words. Front-loads the core purpose (read or update) and optional feature.
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?
No output schema exists, so the description should indicate what the tool returns (e.g., updated status or current status). It fails to do so, leaving the response behavior ambiguous. However, it covers the core functionality adequately.
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 description coverage is 100%, so the description adds no new parameter meaning beyond the structured fields. The mention of 'optionally notifying Slack' aligns with the notify parameter's description.
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 tool reads or updates an annotation task's status, with optional Slack notification. It distinguishes from sibling tools like dynamo_read/write by focusing on annotation status, and from slack_notify by combining status update with notification.
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 annotation status operations but does not explicitly state when to use this tool over siblings (e.g., dynamo_read/write for generic data, slack_notify for standalone notifications). No when-not-to-use guidance provided.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
dynamo_readA
Read a single record from DynamoDB by primary key 'id'.
| Name | Required | Description | Default |
|---|---|---|---|
| id | Yes | Primary key (partition key 'id') of the record | |
| consistentRead | No | Use strongly consistent read |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries full burden. It discloses the tool is read-only (non-destructive) but does not mention any behavioral traits like rate limits, consistency modes beyond the parameter, or error handling. The description is minimal but not contradictory.
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 sentence that is concise and front-loaded with the verb 'Read'. Every word serves a purpose, no fluff.
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?
For a simple tool with two parameters and no output schema, the description covers the essential purpose and key. Could optionally mention the return format, but it's not required for a straightforward read operation.
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 description coverage is 100% with both parameters documented. The description adds 'by primary key id' which matches the schema, but does not add new meaning beyond what the schema provides. Baseline 3 is appropriate.
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 'Read a single record from DynamoDB by primary key id', which specifies the verb (read), resource (DynamoDB record), and the unique key. This distinguishes it from sibling tools like dynamo_write (write) and s3_download (different service).
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 when you have a specific 'id' to retrieve a single record. It does not explicitly state when not to use (e.g., for batch reads) but the purpose is clear enough for basic selection.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
dynamo_writeC
Write a record to DynamoDB keyed by 'id'.
| Name | Required | Description | Default |
|---|---|---|---|
| id | Yes | Primary key 'id' for the record | |
| attributes | Yes | Arbitrary attributes to store with the record | |
| overwrite | No | If false, fail when item already exists |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description must disclose behavioral traits. It mentions keying by id but omits critical details such as the overwrite parameter's effect (default true) and failure behavior when overwrite is false. No side effects or consistency guarantees are described.
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 a single, short sentence that is to the point. It achieves conciseness but could be improved by including more context without sacrificing brevity.
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 output schema, three parameters (including a nested object), and no annotations, the description is incomplete. It does not explain return values, error behavior, or what happens on duplicate ids.
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 schema already adequately describes all three parameters. The description adds 'keyed by id' which is redundant with the schema's primary key description, providing no additional meaning.
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 'Write' and the resource 'a record to DynamoDB keyed by id', distinguishing it from sibling dynamo_read. However, it does not mention the overwrite behavior or additional attributes.
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?
No guidance is provided on when to use this tool versus alternatives like dynamo_read or s3_upload. There are no explicit when-not or context clues.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
s3_downloadB
Download an object from S3 and return it base64-encoded.
| Name | Required | Description | Default |
|---|---|---|---|
| key | Yes | S3 object key/path to fetch | |
| bucket | No | Override default S3 bucket |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries the full burden of behavioral disclosure. It reveals that the output is base64-encoded, which is key behavior, but does not mention limitations (e.g., file size, timeout), permissions required, or whether the tool is safe/read-only. The description adds some value beyond the schema but is insufficient for a no-annotation tool.
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 a single sentence that conveys the essential purpose and key detail (base64 encoding). No extraneous information. Efficient and well-structured.
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 has 2 parameters, no output schema, and no annotations, the description is minimal but covers the core functionality. However, it lacks details on error handling, default bucket behavior, or size limits. For a simple download tool, it is adequate but not comprehensive.
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 input schema provides descriptions for both parameters ('key' and 'bucket') with 100% coverage. The description does not add new meaning beyond the schema; it only restates the purpose. Baseline 3 is appropriate.
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 explicitly states 'Download an object from S3 and return it base64-encoded', which is a specific verb ('download'), resource ('S3 object'), and adds detail about encoding. This clearly distinguishes it from sibling tools like s3_upload (upload) and other non-S3 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?
No guidance is provided on when to use this tool versus alternatives. For example, it does not mention that this is for small objects due to base64 encoding, or contrast with other data access tools like dynamo_read. The description lacks context for when this tool is appropriate.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
s3_uploadB
Upload a base64-encoded object to S3.
| Name | Required | Description | Default |
|---|---|---|---|
| key | Yes | S3 object key/path | |
| contentBase64 | Yes | File contents, base64-encoded | |
| contentType | No | MIME type, e.g. image/png | application/octet-stream |
| bucket | No | Override default S3 bucket |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided. The description implies a write operation but omits important behavior: overwrite semantics, permission requirements, error handling, or side effects like triggering bucket events. The base64 overhead and potential size limits are not mentioned.
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 a single sentence with no wasted words. It is front-loaded with the action and resource. Slightly more detail could be added without sacrificing conciseness.
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 output schema and no annotations, the description should provide more context about the return value, default bucket behavior, and MIME type handling. It lacks completeness for a tool with four parameters.
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 baseline is 3. The description adds no additional meaning beyond the parameter names and types already in the schema. It does not clarify the relationship between 'key' and 'bucket' or the necessity of valid base64 in 'contentBase64'.
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 action ('Upload'), the resource ('object to S3'), and the encoding requirement ('base64-encoded'). It distinguishes from the sibling tool s3_download, which downloads objects.
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?
No guidance on when to use this tool versus alternatives, prerequisites (e.g., AWS credentials), or constraints (e.g., object size limits). The description does not contrast with s3_download or other siblings.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
slack_notifyA
Post a message to a Slack channel.
| Name | Required | Description | Default |
|---|---|---|---|
| message | Yes | Text to post to Slack | |
| channel | No | Channel ID or name; defaults to SLACK_DEFAULT_CHANNEL | |
| threadTs | No | Optional parent message ts to reply in a thread |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description must fully convey behavioral traits. It only states the basic action without mentioning authentication requirements, rate limits, whether messages are formatted, or error behavior. This is insufficient for an agent to understand side effects or constraints.
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 a single, concise sentence that front-loads the action and resource. Every word is necessary and there is no wasted text.
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 absence of an output schema and annotations, the description is minimal. It covers the core purpose but lacks details on limitations (e.g., message length), return values, or confirmation of success. It is adequate for a simple tool but not comprehensive.
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 description coverage is 100%; the schema already documents each parameter (message, channel, threadTs) with descriptions. The tool description adds no additional meaning beyond what is in the schema, so a baseline score of 3 is appropriate.
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 action ('Post a message') and the resource ('to a Slack channel'). It is a specific verb+resource pair that distinguishes this tool from siblings like dynamo_read or s3_upload, which operate on different services.
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 does not provide explicit guidance on when to use this tool versus alternatives. While the siblings are dissimilar, there is no mention of prerequisites or context (e.g., 'use this to send notifications, not for file uploads'). Usage is implied but not clarified.
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.
6 tool updates
v1.0.0- First observed
annotation_status - First observed
dynamo_read - First observed
dynamo_write - First observed
s3_download - First observed
s3_upload - First observed
slack_notify
TDQS
Scored across 6 tools
Each tool has a clearly distinct purpose: annotation_status for task status, dynamo_read/write for database, s3_download/upload for storage, slack_notify for messaging. No overlap.
Most tools follow verb_noun pattern (dynamo_read, dynamo_write, s3_download, s3_upload, slack_notify), but 'annotation_status' is an outlier as a noun phrase.
6 tools is well-scoped for a server covering annotation, database, storage, and Slack integration. It feels neither too few nor too many.
Each service has basic operations but lacks deletions, updates, and lists. For annotation only status is provided. This leaves notable gaps for typical workflows, but the server may be intentionally scoped.
Maintenance
Related MCP Connectors
Hosted MCP server to manage a restaurant menu from AI agents - 39 tools over the DuckHub API.
Hosted MCP server for MuntuAI outreach campaigns, leads, senders, domains, and analytics.
MCP server that lets AI assistants use all OneSchema features exposed via the public API.
MCP server for Riveter's enrichment, scraping, and monitoring API
Related MCP Servers
- AlicenseAqualityDmaintenanceA production-grade MCP server that enables AI assistants like Claude to read Etsy shop data—listings, orders, inventory, and stats—through eight read-only tools, using OAuth and automatic pagination.8MIT
- AlicenseNot gradedqualityBmaintenanceA production-ready MCP server for the Slack API that enables searching, listing channels, reading history, inspecting users, fetching threads, and sending messages through controlled Slack tools.21,724 npmMIT
- FlicenseBqualityBmaintenanceUnified MCP server exposing 12 DevOps tools across GitHub, PostgreSQL, Slack, and Google Calendar for AI agents, with rate limiting, input validation, and per-service scoped tokens.30-
- FlicenseNot gradedqualityCmaintenanceA remote, OAuth2-authenticated MCP server that provides read-only S3 tools (list buckets, list objects, check public access, get bucket size) designed to run on ECS Fargate with Auth0 as the identity provider.-