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
40 changes: 40 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: CI

on:
pull_request:
push:
branches:
- main

permissions:
contents: read

jobs:
test:
name: ${{ matrix.os }} / Node ${{ matrix.node }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-24.04
- windows-2025
node:
- "20.18.1"
- "22"
- "24"
steps:
- name: Check out source
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false

- name: Set up Node.js
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: ${{ matrix.node }}
package-manager-cache: false

- run: npm ci
- run: npm test
- run: npm run package:audit
97 changes: 97 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: Release

on:
push:
tags:
- "v*.*.*"

permissions:
contents: read
id-token: write

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
publish:
name: Publish verified package
runs-on: ubuntu-24.04
steps:
- name: Check out tagged source
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
fetch-depth: 0
persist-credentials: false

- name: Require the tagged commit to be on main
run: |
set -euo pipefail
if ! git show-ref --verify --quiet refs/remotes/origin/main; then
echo "origin/main was not fetched" >&2
exit 1
fi
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
echo "Tagged commit $GITHUB_SHA is not an ancestor of origin/main" >&2
exit 1
fi

- name: Set up Node.js
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: "24"
registry-url: https://registry.npmjs.org
package-manager-cache: false

- name: Use the trusted-publishing npm CLI
run: |
npm install --global npm@12.0.2
test "$(npm --version)" = "12.0.2"

- name: Require an exact stable release tag
env:
RELEASE_TAG: ${{ github.ref_name }}
run: |
node --input-type=module <<'NODE'
import { readFileSync } from "node:fs";

const manifest = JSON.parse(readFileSync("package.json", "utf8"));
if (!/^\d+\.\d+\.\d+$/.test(manifest.version)) {
throw new Error(`Release version must be stable SemVer, received ${manifest.version}`);
}

const expectedTag = `v${manifest.version}`;
if (process.env.RELEASE_TAG !== expectedTag) {
throw new Error(`Tag ${process.env.RELEASE_TAG} does not match package version ${expectedTag}`);
}
NODE

- run: npm ci
- run: npm test

- name: Pack, audit, and publish the exact release artifact
run: |
set -euo pipefail
artifact_dir="$(mktemp -d "$RUNNER_TEMP/agentcommunity-mcp-release.XXXXXX")"
trap 'rm -rf -- "$artifact_dir"' EXIT
pack_result="$artifact_dir/pack-result.json"
npm pack --json --pack-destination "$artifact_dir" > "$pack_result"
tarball="$(
PACK_RESULT="$pack_result" ARTIFACT_DIR="$artifact_dir" node --input-type=module <<'NODE'
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";

import { normalizePackResult } from "./scripts/audit-package.mjs";

const manifest = JSON.parse(readFileSync("package.json", "utf8"));
const results = JSON.parse(readFileSync(process.env.PACK_RESULT, "utf8"));
const result = normalizePackResult(results, manifest.version);
const tarball = join(process.env.ARTIFACT_DIR, result.filename);
if (!existsSync(tarball)) throw new Error("Packed artifact is missing");

process.stdout.write(tarball);
NODE
)"
sha256sum "$tarball"
npm run package:audit -- --tarball "$tarball" --pack-result "$pack_result"
npm publish "$tarball" --access public --provenance
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
*.tgz
pack-result.json
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Repository boundary

This repository owns only the `@agentcommunity/mcp` stdio connector. The hosted server and its tools live at `https://agentcommunity.org/mcp`; do not copy server logic here. The connector must remain anonymous, fixed-origin, telemetry-free, and dependency-pinned. It must never call a tool during tests or installation, expose an SDK API, accept an alternate remote URL, add install scripts, or store credentials. The sole local-publish exception is the reviewed `0.1.0` bootstrap required to create the npm package before trusted publishing can be attached; it must be deprecated after `0.1.1` is live. Every later publication, including canonical `0.1.1`, uses the tag-only OIDC workflow after `npm test` and `npm run package:audit` pass.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Agent Community

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,20 @@
# mcp
Official stdio connector for the Agent Community hosted MCP server
# Agent Community MCP connector

`@agentcommunity/mcp` is the official stdio compatibility connector for Agent Community's hosted MCP server at `https://agentcommunity.org/mcp`. It is published by Agent Community from `https://github.com/agentcommunity/mcp`.

It does not implement or copy the server, expose an SDK API, add authentication, or call a tool during startup. It launches the pinned `mcp-remote` transport bridge so stdio-only clients can use the existing anonymous Streamable HTTP endpoint.

## Run

```sh
npx -y @agentcommunity/mcp
```

The installed executable is `agentcommunity-mcp`. Clients with native remote Streamable HTTP support should connect directly to `https://agentcommunity.org/mcp` instead.

## Verify ownership

- Homepage and documentation: https://agentcommunity.org/developers
- Source: https://github.com/agentcommunity/mcp
- Package: https://www.npmjs.com/package/@agentcommunity/mcp
- Hosted server card: https://agentcommunity.org/.well-known/mcp/server-card.json
11 changes: 11 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Security Policy

## Supported versions

The `0.1.x` release line is supported with security updates.

## Reporting a vulnerability

Report vulnerabilities privately to [security@agentcommunity.org](mailto:security@agentcommunity.org).

Do not include secrets, credentials, private keys, tokens, or other sensitive information in public GitHub issues.
93 changes: 93 additions & 0 deletions bin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#!/usr/bin/env node
import { spawn } from 'node:child_process';
import { readFileSync, realpathSync } from 'node:fs';
import { createRequire } from 'node:module';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const require = createRequire(import.meta.url);
const remoteManifestPath = require.resolve('mcp-remote/package.json');
const remoteManifest = JSON.parse(readFileSync(remoteManifestPath, 'utf8'));
const remoteBin = remoteManifest.bin?.['mcp-remote'];

if (typeof remoteBin !== 'string') {
throw new Error('Installed mcp-remote package does not expose its expected executable.');
}

const remoteExecutable = join(dirname(remoteManifestPath), remoteBin);
const remoteUrl = 'https://agentcommunity.org/mcp';

export const helpText = `Official Agent Community MCP connector

Usage: agentcommunity-mcp [mcp-remote options]

Bridges a local stdio MCP client to ${remoteUrl}.
The hosted endpoint is anonymous; this connector stores no credentials.
`;

export function buildInvocation(argv = process.argv.slice(2)) {
return {
command: process.execPath,
args: [remoteExecutable, remoteUrl, ...argv],
};
}

const terminationSignals = ['SIGINT', 'SIGTERM', 'SIGHUP'];

export function installSignalForwarding(child, hostProcess = process) {
const handlers = new Map();
for (const signal of terminationSignals) {
const handler = function () {
child.kill(signal);
};
handlers.set(signal, handler);
hostProcess.on(signal, handler);
}
return function removeSignalForwarding() {
for (const [signal, handler] of handlers) hostProcess.off(signal, handler);
};
}

export function applyChildExit(code, signal, hostProcess = process) {
if (signal !== null) {
hostProcess.kill(hostProcess.pid, signal);
return;
}
hostProcess.exitCode = code ?? 1;
}

export function main(argv = process.argv.slice(2)) {
if (argv.includes('--help') || argv.includes('-h')) {
process.stdout.write(helpText);
return;
}

const invocation = buildInvocation(argv);
const child = spawn(invocation.command, invocation.args, {
stdio: 'inherit',
env: process.env,
});
const removeSignalForwarding = installSignalForwarding(child);

child.once('error', function (error) {
removeSignalForwarding();
process.stderr.write(`agentcommunity-mcp: ${error.message}\n`);
process.exitCode = 1;
});
child.once('exit', function (code, signal) {
removeSignalForwarding();
applyChildExit(code, signal);
});
}

function isMainModule() {
if (typeof process.argv[1] !== 'string') return false;

try {
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
} catch {
return false;
}
}

if (isMainModule()) main();
Loading