@conciergeprotocol/mcp 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,45 @@
1
- # Temporary Holding Version
1
+ # @conciergeprotocol/mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ An MCP server that asks a [Concierge Agent Protocol](https://conciergeprotocol.org) front door. Claude, Cursor, and other MCP hosts get three tools. The company publishes `agent.txt`. This package calls it.
4
+
5
+ The protocol is at [conciergeprotocol.org](https://conciergeprotocol.org). Source is at [github.com/conciergeprotocol/js](https://github.com/conciergeprotocol/js). The library underneath is [@conciergeprotocol/client](https://www.npmjs.com/package/@conciergeprotocol/client).
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install @conciergeprotocol/mcp
11
+ ```
12
+
13
+ Or run it without a global install. Cursor, Claude Desktop, and VS Code use the same block. In VS Code it goes in `.vscode/mcp.json`. In Cursor and Claude Desktop it goes in the MCP settings file.
14
+
15
+ ```json
16
+ {
17
+ "mcpServers": {
18
+ "cap": {
19
+ "command": "npx",
20
+ "args": ["-y", "@conciergeprotocol/mcp"],
21
+ "env": { "CAP_FROM": "agent:my-assistant" }
22
+ }
23
+ }
24
+ }
25
+ ```
26
+
27
+ `CAP_FROM` names the caller. It must look like `agent:my-assistant`. When it is unset, the server uses `agent:cap-mcp`.
28
+
29
+ Node.js 18 or newer.
30
+
31
+ ## Tools
32
+
33
+ | Tool | What it does |
34
+ | --- | --- |
35
+ | `cap_discover` | Reads `agent.txt` and reports whether the domain has a front door. |
36
+ | `cap_ask` | Sends one question and waits up to 60 seconds. |
37
+ | `cap_check` | Reads a message that was still `working`. |
38
+
39
+ `cap_ask` sends only the new question. Pass the previous message id for a follow-up. If the company is still answering, the tool returns the message id and no reply. Call `cap_check`. Do not guess the answer.
40
+
41
+ A domain with no `agent.txt` comes back as a normal result, which is the signal to browse the site.
42
+
43
+ ## License
44
+
45
+ Apache 2.0
package/package.json CHANGED
@@ -1,6 +1,37 @@
1
1
  {
2
2
  "name": "@conciergeprotocol/mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "MCP server for asking a Concierge Agent Protocol front door.",
5
+ "type": "module",
6
+ "bin": {
7
+ "cap-mcp": "./src/index.js"
8
+ },
9
+ "files": [
10
+ "src"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "license": "Apache-2.0",
16
+ "homepage": "https://conciergeprotocol.org",
17
+ "keywords": [
18
+ "concierge",
19
+ "cap",
20
+ "mcp",
21
+ "agents",
22
+ "model-context-protocol"
23
+ ],
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/conciergeprotocol/js.git",
27
+ "directory": "packages/mcp"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "dependencies": {
33
+ "@conciergeprotocol/client": "workspace:*",
34
+ "@modelcontextprotocol/sdk": "1.32.1",
35
+ "zod": "^4.6.7"
36
+ }
37
+ }
package/src/index.js ADDED
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ import { createClient } from '@conciergeprotocol/client'
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
5
+ import * as z from 'zod/v4'
6
+ import { createTools, toolError } from './tools.js'
7
+
8
+ const FROM = /^agent:[A-Za-z0-9_.-]{1,64}$/
9
+ const from = process.env.CAP_FROM || 'agent:cap-mcp'
10
+ if (!FROM.test(from)) {
11
+ console.error('CAP_FROM must be agent:{id}.')
12
+ process.exit(1)
13
+ }
14
+
15
+ const ASK = [
16
+ 'Ask a company that publishes a CAP front door.',
17
+ 'Use this when the person asks about a specific company, product, store, or organization with a known website. Prefer it to browsing when the site has a front door.',
18
+ 'Send only the new question in a follow-up, with conversation_id.',
19
+ 'Never write an answer for the site while it is still answering.',
20
+ 'The reply is the site\'s own framing. For comparisons and reviews, check other sources too.',
21
+ 'Never put the person\'s password or another site\'s data in context.',
22
+ ].join(' ')
23
+
24
+ const tools = createTools(createClient({ from, maxWaitMs: 60_000 }))
25
+
26
+ function ok(value) {
27
+ return { content: [{ type: 'text', text: JSON.stringify(value) }] }
28
+ }
29
+
30
+ function fail(err) {
31
+ return { isError: true, content: [{ type: 'text', text: toolError(err) }] }
32
+ }
33
+
34
+ const server = new McpServer({ name: 'cap', version: '0.1.0' })
35
+
36
+ server.registerTool('cap_discover', {
37
+ description: 'Check whether a website has a CAP front door, and whether visitors can log in.',
38
+ inputSchema: { domain: z.string().describe('The company domain, such as cosmicjs.com') },
39
+ }, async (args) => {
40
+ try {
41
+ return ok(await tools.cap_discover(args))
42
+ } catch (err) {
43
+ return fail(err)
44
+ }
45
+ })
46
+
47
+ server.registerTool('cap_ask', {
48
+ description: ASK,
49
+ inputSchema: {
50
+ domain: z.string().describe('The company domain, such as cosmicjs.com'),
51
+ message: z.string().describe('The question to send. On a follow-up, send only the new question.'),
52
+ context: z.record(z.string(), z.unknown()).optional().describe('A short brief such as sizes, dates, or style. Not a profile.'),
53
+ conversation_id: z.string().optional().describe('The conversation_id from the previous cap_ask, when this continues that conversation.'),
54
+ },
55
+ }, async (args) => {
56
+ try {
57
+ return ok(await tools.cap_ask(args))
58
+ } catch (err) {
59
+ return fail(err)
60
+ }
61
+ })
62
+
63
+ server.registerTool('cap_check', {
64
+ description: 'Read a CAP message that was still being answered. Do not guess a reply while the status is working.',
65
+ inputSchema: {
66
+ domain: z.string().describe('The company domain used in cap_ask'),
67
+ message_id: z.string().describe('The message_id returned by cap_ask'),
68
+ },
69
+ }, async (args) => {
70
+ try {
71
+ return ok(await tools.cap_check(args))
72
+ } catch (err) {
73
+ return fail(err)
74
+ }
75
+ })
76
+
77
+ const transport = new StdioServerTransport()
78
+ await server.connect(transport)
package/src/tools.js ADDED
@@ -0,0 +1,58 @@
1
+ import { CapError, NoDoorError } from '@conciergeprotocol/client'
2
+
3
+ const ASK_HINT = 'The company is still answering. Call cap_check with this message_id. Do not write a reply for the company.'
4
+
5
+ export function createTools(cap) {
6
+ return {
7
+ async cap_discover({ domain }) {
8
+ const door = await cap.discover(domain)
9
+ if (!door) return { door: false }
10
+ return {
11
+ door: true,
12
+ name: door.name,
13
+ chat: door.chat,
14
+ login: Boolean(door.oauth),
15
+ }
16
+ },
17
+
18
+ async cap_ask({ domain, message, context, conversation_id }) {
19
+ try {
20
+ const answer = await cap.ask(domain, message, {
21
+ context,
22
+ previousMessageId: conversation_id,
23
+ })
24
+ if (answer.status === 'working') {
25
+ return {
26
+ status: 'working',
27
+ message_id: answer.id,
28
+ conversation_id: answer.id,
29
+ message: ASK_HINT,
30
+ }
31
+ }
32
+ if (answer.status === 'failed') {
33
+ return { status: 'failed', error: answer.error, conversation_id: answer.id }
34
+ }
35
+ return { status: 'done', reply: answer.reply, conversation_id: answer.id }
36
+ } catch (err) {
37
+ if (err instanceof NoDoorError) {
38
+ return { door: false, message: `${domain} has no CAP front door. Browse the site instead.` }
39
+ }
40
+ throw err
41
+ }
42
+ },
43
+
44
+ async cap_check({ domain, message_id }) {
45
+ const answer = await cap.get(domain, message_id)
46
+ if (answer.status === 'working') {
47
+ return { status: 'working', message_id: answer.id, message: ASK_HINT }
48
+ }
49
+ if (answer.status === 'failed') return { status: 'failed', error: answer.error, message_id: answer.id }
50
+ return { status: 'done', reply: answer.reply, message_id: answer.id }
51
+ },
52
+ }
53
+ }
54
+
55
+ export function toolError(err) {
56
+ if (err instanceof CapError) return err.message
57
+ return 'The front door could not be reached.'
58
+ }