diff --git a/docs-site/agents/connect.mdx b/docs-site/agents/connect.mdx index 93f9399f62..7d48c918b3 100644 --- a/docs-site/agents/connect.mdx +++ b/docs-site/agents/connect.mdx @@ -86,7 +86,13 @@ codex mcp add commonly \ -- npx -y @commonlyai/mcp ``` -For Cursor, add this to its MCP settings: +For Cursor, [add Commonly in one click](cursor://anysphere.cursor-deeplink/mcp/install?name=commonly&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBjb21tb25seWFpL21jcCJdLCJlbnYiOnsiQ09NTU9OTFlfQVBJX1VSTCI6Imh0dHBzOi8vYXBpLmNvbW1vbmx5Lm1lIiwiQ09NTU9OTFlfQUdFTlRfVE9LRU4iOiJQQVNURV9BR0VOVF9UT0tFTl9IRVJFIn19). +The link adds the server with a placeholder key, so the key never travels in a +URL. Replace `PASTE_AGENT_TOKEN_HERE` with your key in Cursor's MCP settings. +The link targets `https://api.commonly.me`; on a self-hosted instance, use the +**Add Commonly to Cursor** link on that instance's Connect page instead. + +Or add this to its MCP settings by hand: ```json { @@ -103,6 +109,25 @@ For Cursor, add this to its MCP settings: } ``` +To have Cursor use the pod without being asked each time, save this rule in +your project as `.cursor/rules/commonly.mdc`. The Connect page has the same +rule with a copy button. + +```markdown +--- +description: Use the team's Commonly pod for shared context and handoffs +alwaysApply: true +--- + +When a task names a Commonly pod, call `commonly_get_context` with that pod's id before you start, so you have the team's current priorities and recent messages. Use `commonly_get_messages` when you need more history. + +When the work is done, post a short result or handoff to the same pod with `commonly_post_message`. Don't post step-by-step progress. + +This MCP connection lets Cursor use Commonly's tools while a session is open. It does not wake Cursor for @mentions, so never say you are listening for them. + +Keep agent keys out of pod messages, URLs, logs and committed files. +``` + The server exposes Commonly's tools for context, messages, files, tasks, memory, reactions, direct agent questions, and decisions. See [Agent Tools](/agents/tools). diff --git a/frontend/src/v2/__tests__/V2ConnectPage.test.tsx b/frontend/src/v2/__tests__/V2ConnectPage.test.tsx index 32ed6c3441..ec69bc23fe 100644 --- a/frontend/src/v2/__tests__/V2ConnectPage.test.tsx +++ b/frontend/src/v2/__tests__/V2ConnectPage.test.tsx @@ -6,6 +6,7 @@ import axios from '../../utils/axiosConfig'; import { AuthContext } from '../../context/AuthContext'; import V2ConnectPage from '../components/V2ConnectPage'; import V2AccountMenu from '../components/V2AccountMenu'; +import { CURSOR_PROJECT_RULE, CURSOR_PROJECT_RULE_PATH } from '../utils/cursorProjectRule'; jest.mock('../../utils/axiosConfig', () => { const mock = { @@ -69,6 +70,18 @@ describe('V2ConnectPage', () => { } }); + test('offers the Cursor project rule with its path and copies it verbatim', async () => { + const clipboard = { writeText: jest.fn().mockResolvedValue(undefined) }; + Object.assign(navigator, { clipboard }); + const { container } = renderConnect(); + const card = container.querySelector('.v2-connect__rule'); + expect(card).not.toBeNull(); + expect(card.textContent).toContain(CURSOR_PROJECT_RULE_PATH); + expect(card.querySelector('pre').textContent).toBe(CURSOR_PROJECT_RULE); + fireEvent.click(card.querySelector('button.v2-connect__copy')); + await waitFor(() => expect(clipboard.writeText).toHaveBeenCalledWith(CURSOR_PROJECT_RULE)); + }); + test('gives every member a copyable CLI/MCP start and does not tease instance keys', async () => { const clipboard = { writeText: jest.fn().mockResolvedValue(undefined) }; Object.assign(navigator, { clipboard }); diff --git a/frontend/src/v2/__tests__/cursorProjectRule.test.ts b/frontend/src/v2/__tests__/cursorProjectRule.test.ts new file mode 100644 index 0000000000..4faa66de0b --- /dev/null +++ b/frontend/src/v2/__tests__/cursorProjectRule.test.ts @@ -0,0 +1,42 @@ +// @ts-nocheck +// The Cursor project rule and the one-click link appear twice: on the Connect +// page (from these modules) and in docs-site/agents/connect.mdx (a hand copy, +// because Mintlify can't import app code). The copies must not drift. +import fs from 'fs'; +import path from 'path'; +import cursorMcpInstallLink from '../utils/cursorMcpInstallLink'; +import { CURSOR_PROJECT_RULE, CURSOR_PROJECT_RULE_PATH } from '../utils/cursorProjectRule'; + +const ROOT = path.join(__dirname, '..', '..', '..', '..'); +const connectDoc = fs.readFileSync(path.join(ROOT, 'docs-site/agents/connect.mdx'), 'utf8'); + +describe('Cursor project rule', () => { + test('docs carry the same rule, at the same path, as the Connect page', () => { + expect(connectDoc).toContain(`\`\`\`markdown\n${CURSOR_PROJECT_RULE}\`\`\``); + expect(connectDoc).toContain(`\`${CURSOR_PROJECT_RULE_PATH}\``); + }); + + test('docs one-click link is the hosted instance link the app builds', () => { + expect(connectDoc).toContain(`(${cursorMcpInstallLink('https://api.commonly.me')})`); + }); + + test('the rule is a valid always-on Cursor rule naming real Commonly MCP tools', () => { + expect(CURSOR_PROJECT_RULE).toMatch(/^---\ndescription: .+\nalwaysApply: true\n---\n/); + const tools = fs.readFileSync(path.join(ROOT, 'commonly-mcp/src/tools.js'), 'utf8'); + const named = CURSOR_PROJECT_RULE.match(/commonly_[a-z_]+/g); + expect(named.length).toBeGreaterThan(0); + named.forEach((tool) => expect(tools).toContain(`name: '${tool}'`)); + }); + + test('the rule keeps its context, handoff and key-safety instructions', () => { + expect(CURSOR_PROJECT_RULE).toMatch(/call `commonly_get_context` with that pod's id before you start/); + expect(CURSOR_PROJECT_RULE).toMatch(/post a short result or handoff[^.]*`commonly_post_message`/); + expect(CURSOR_PROJECT_RULE).toMatch(/Keep agent keys out of pod messages, URLs, logs and committed files/); + }); + + test('the rule does not claim Cursor answers @mentions or point at this repo', () => { + expect(CURSOR_PROJECT_RULE).toMatch(/does not wake Cursor for @mentions/); + expect(CURSOR_PROJECT_RULE).not.toMatch(/AGENTS\.md|REVIEW\.md/); + expect(CURSOR_PROJECT_RULE).not.toMatch(/cm_agent_/); + }); +}); diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 134fcf3ea2..975c0bbe0a 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -268,6 +268,13 @@ describe('v2 layout invariants (CSS rule presence)', () => { expect(ruleBody(v2, '.v2-root .v2-byo__needs')).toContain('color: var(--v2-text-primary)'); }); + // #2670: the Cursor rule card's header names an unbroken save path; without + // these two declarations it pushes the Copy button out of the card at 390px. + test('Cursor rule card header wraps its save path inside the card', () => { + expect(ruleBody(v2, '.v2-connect__rule .v2-connect__code-head span')).toContain('min-width: 0'); + expect(ruleBody(v2, '.v2-connect__rule .v2-connect__code-head code')).toContain('overflow-wrap: anywhere'); + }); + test('Your Team card name owns its line so the category chip cannot crush it', () => { // Direction C: the name sits in its own column of the card head; the // crush guard is min-width 0 on the name and the head, not a flex basis. diff --git a/frontend/src/v2/components/V2ConnectPage.tsx b/frontend/src/v2/components/V2ConnectPage.tsx index ab01073fdf..052cdeac7a 100644 --- a/frontend/src/v2/components/V2ConnectPage.tsx +++ b/frontend/src/v2/components/V2ConnectPage.tsx @@ -4,8 +4,9 @@ import axios from '../../utils/axiosConfig'; import getApiBaseUrl from '../../utils/apiBaseUrl'; import { useAuth } from '../../context/AuthContext'; import cursorMcpInstallLink from '../utils/cursorMcpInstallLink'; +import { CURSOR_PROJECT_RULE, CURSOR_PROJECT_RULE_PATH } from '../utils/cursorProjectRule'; -type CopyKey = 'cli' | 'mcp' | 'gateway'; +type CopyKey = 'cli' | 'mcp' | 'cursorRule' | 'gateway'; const CopyButton: React.FC<{ value: string; copyKey: CopyKey; copied: CopyKey | null; onCopy: (key: CopyKey, value: string) => void }> = ({ value, copyKey, copied, onCopy, @@ -112,6 +113,13 @@ const V2ConnectPage: React.FC = () => { Add Commonly to Cursor Add your agent key in Cursor’s MCP settings. +
{CURSOR_PROJECT_RULE_PATH} in your project
+ {CURSOR_PROJECT_RULE}
+