Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Bug report
description: Report something that is not working as expected
title: "[Bug]: "
labels:
- bug
body:
- type: markdown
attributes:
value: |
Thanks for helping improve Substack MCP. Never paste your session token or a complete Cookie header into an issue.

- type: input
id: version
attributes:
label: Substack MCP version
description: Run `npm view substack-mcp version` or provide the Docker image tag you use.
placeholder: 1.3.0
validations:
required: true

- type: dropdown
id: installation
attributes:
label: Installation method
options:
- NPX
- Docker
- From source
- Other
validations:
required: true

- type: input
id: client
attributes:
label: MCP client
description: Include the client version when possible.
placeholder: Claude Desktop 1.x, Cursor 1.x, VS Code 1.x, etc.

- type: input
id: environment
attributes:
label: Operating system and runtime
description: Include your OS and, for NPX or source installs, the output of `node --version`.
placeholder: macOS 15.6, Node.js v22.18.0

- type: textarea
id: reproduction
attributes:
label: Steps to reproduce
description: Tell us which tool was called, with which non-secret inputs, and what happened.
placeholder: |
1. Configure the server using NPX
2. Ask the client to list recent drafts
3. The call fails with ...
validations:
required: true

- type: textarea
id: expected
attributes:
label: Expected behavior

- type: textarea
id: logs
attributes:
label: Sanitized logs
description: Paste the relevant JSON log lines. Remove tokens, cookies, email addresses, post content, and other private data first.
render: shell

- type: checkboxes
id: secrets
attributes:
label: Security check
options:
- label: I removed session tokens, Cookie headers, email addresses, and other private data from this report.
required: true
77 changes: 73 additions & 4 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ on:
tags: [ 'v*' ]

jobs:
build-and-publish:
name: Build and Publish
publish-npm:
name: Publish to npm
runs-on: ubuntu-latest
permissions:
contents: read

steps:
- name: Checkout code
Expand All @@ -18,8 +20,75 @@ jobs:
uses: actions/setup-node@v7
with:
node-version-file: '.nvmrc'

- name: Run publish script
run: ./ops/publish-npm.sh
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

publish-registry:
name: Publish MCP Registry metadata
needs: publish-npm
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write

steps:
- name: Checkout code
uses: actions/checkout@v7

- name: Prepare MCP Registry metadata
run: |
PACKAGE_VERSION=$(jq -r '.version' package.json)
jq --arg version "$PACKAGE_VERSION" \
'.version = $version
| (.packages[] | select(.registryType == "npm").version) = $version
| (.packages[] | select(.registryType == "oci").identifier) = "docker.io/marcomoauro/substack-mcp:v\($version)"' \
server.json > "$RUNNER_TEMP/server.json"

- name: Wait for the versioned Docker image
env:
MCP_SERVER_NAME: io.github.marcomoauro/substack-mcp
run: |
PACKAGE_VERSION=$(jq -r '.version' package.json)
IMAGE="docker.io/marcomoauro/substack-mcp:v${PACKAGE_VERSION}"

for attempt in $(seq 1 30); do
if docker pull "$IMAGE" >/dev/null 2>&1; then
LABEL=$(docker inspect --format '{{ index .Config.Labels "io.modelcontextprotocol.server.name" }}' "$IMAGE")
if [ "$LABEL" = "$MCP_SERVER_NAME" ]; then
echo "$IMAGE is available with the required ownership label"
exit 0
fi
fi

if [ "$attempt" -lt 30 ]; then
echo "Waiting for $IMAGE (attempt $attempt/30)"
sleep 10
fi
done

echo "Error: $IMAGE was not published with ownership label $MCP_SERVER_NAME"
exit 1

- name: Install MCP Registry publisher
env:
MCP_PUBLISHER_VERSION: 1.8.1
run: |
ARCHIVE=mcp-publisher_linux_amd64.tar.gz
RELEASE_URL="https://github.com/modelcontextprotocol/registry/releases/download/v${MCP_PUBLISHER_VERSION}"

cd "$RUNNER_TEMP"
curl -fsSLO "$RELEASE_URL/$ARCHIVE"
curl -fsSL "$RELEASE_URL/registry_${MCP_PUBLISHER_VERSION}_checksums.txt" -o checksums.txt
awk -v file="$ARCHIVE" '$2 == file' checksums.txt | sha256sum --check -
tar xzf "$ARCHIVE" mcp-publisher

- name: Authenticate to the MCP Registry
run: |
"$RUNNER_TEMP/mcp-publisher" login github-oidc

- name: Publish to the MCP Registry
run: |
"$RUNNER_TEMP/mcp-publisher" publish "$RUNNER_TEMP/server.json"
16 changes: 16 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,22 @@ noisier, tally `# pass`, failures `not ok`. Grep for both, or a perfectly green
version comes back empty and reads as a broken command: `grep -E '^(#|ℹ) (tests|pass|fail)'` for
the tally, `grep -E '^(not ok|✖)'` for what broke.

## Distribution and releases

Release tags publish three artifacts: the npm package through `.github/workflows/npm-publish.yml`,
the multi-architecture Docker image through `.github/workflows/docker-build-push.yml`, and metadata
for both installation methods to the official MCP Registry. The registry job is separate from the
npm job so a registry failure can be rerun without attempting to publish the same immutable npm
version twice. It waits for the independently built versioned Docker image before publishing the
combined metadata.

`package.json`'s `mcpName` and the Dockerfile label
`io.modelcontextprotocol.server.name` are ownership proofs. Both must equal
`io.github.marcomoauro/substack-mcp`; removing either makes the corresponding package fail registry
publication. `server.json` is the source metadata, but its checked-in version is not a second release
version to bump by hand: the registry job derives the top-level version, the npm package version and
the `v<version>` Docker identifier from `package.json` before publishing.

## Layout

- `src/index.js` — entrypoint only: env check, `createServer()`, stdio transport. Keep it thin.
Expand Down
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
FROM node:24.19.0-alpine

LABEL io.modelcontextprotocol.server.name="io.github.marcomoauro/substack-mcp"

COPY ./ /opt
WORKDIR /opt

RUN npm ci --omit=dev && \
npm cache clean --force;

CMD ["node", "src/index.js"]
CMD ["node", "src/index.js"]
Loading