@joinquest/mcp-integration 0.1.0
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 +111 -0
- package/bin/joinquest-integration-mcp-cursor +11 -0
- package/package.json +48 -0
- package/src/config.js +21 -0
- package/src/example-provision.js +36 -0
- package/src/graphql.js +146 -0
- package/src/index.js +25 -0
- package/src/tools.js +206 -0
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# JoinQuest integration MCP server
|
|
2
|
+
|
|
3
|
+
stdio MCP server for **game developers** integrating with JoinQuest. Exposes developer dashboard operations to Cursor, Claude, and other agents via the same GraphQL API as the portal.
|
|
4
|
+
|
|
5
|
+
## Quick setup
|
|
6
|
+
|
|
7
|
+
1. Sign in at [JoinQuest](https://joinquest.cc) and open your **developer dashboard**.
|
|
8
|
+
2. **Connect an AI assistant** → **Show setup** → **Generate API key** (copy once).
|
|
9
|
+
3. Add MCP config (Node.js 20+, uses `npx` — no install step):
|
|
10
|
+
|
|
11
|
+
**Cursor** — `.cursor/mcp.json`:
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"mcpServers": {
|
|
16
|
+
"joinquest-integration": {
|
|
17
|
+
"type": "stdio",
|
|
18
|
+
"command": "npx",
|
|
19
|
+
"args": ["-y", "@joinquest/mcp-integration", "joinquest-integration-mcp-cursor"],
|
|
20
|
+
"env": {
|
|
21
|
+
"JOINQUEST_API_KEY": "lq_dev_..."
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Claude Code:**
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
claude mcp add --scope project --transport stdio \
|
|
32
|
+
--env JOINQUEST_API_KEY=lq_dev_... \
|
|
33
|
+
joinquest-integration -- npx -y @joinquest/mcp-integration
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Other stdio clients** — use `npx` with args `["-y", "@joinquest/mcp-integration"]` and the same `env`.
|
|
37
|
+
|
|
38
|
+
4. Fully quit and reopen your agent client. Test: ask the agent to call `joinquest_integration_list_my_games`.
|
|
39
|
+
|
|
40
|
+
### Why a separate Cursor bin?
|
|
41
|
+
|
|
42
|
+
Cursor injects `ELECTRON_RUN_AS_NODE` into MCP child processes. Use the second npx arg `joinquest-integration-mcp-cursor` (wrapper that unsets it). Other clients use the default bin.
|
|
43
|
+
|
|
44
|
+
### Optional global install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
curl -fsSL https://raw.githubusercontent.com/scruffyprodigy/playhub/main/scripts/install-joinquest-mcp.sh | sh
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Environment
|
|
51
|
+
|
|
52
|
+
| Variable | Required | Description |
|
|
53
|
+
|----------|----------|-------------|
|
|
54
|
+
| `JOINQUEST_API_KEY` | Yes* | From developer dashboard → Connect AI assistant |
|
|
55
|
+
| `JOINQUEST_ISSUER_URL` | No | Lobby JWT issuer / provision `lobbyId` |
|
|
56
|
+
| `JOINQUEST_PUBLIC_URL` | No | Browser Lobby URL for example provision payloads |
|
|
57
|
+
| `JOINQUEST_SESSION` | Legacy | Session cookie (prefer API key) |
|
|
58
|
+
|
|
59
|
+
Advanced (lobby contributors): `JOINQUEST_API_URL=http://localhost:8080/graphql` — see [docs/development.md](../../docs/development.md).
|
|
60
|
+
|
|
61
|
+
## Publishing
|
|
62
|
+
|
|
63
|
+
Maintainers: `./scripts/publish-joinquest-mcp.sh` (requires npm login + `@joinquest` scope). Roadmap: [docs/developer-ai-setup-roadmap.md](../../docs/developer-ai-setup-roadmap.md).
|
|
64
|
+
|
|
65
|
+
## Client config paths
|
|
66
|
+
|
|
67
|
+
| Client | Config file |
|
|
68
|
+
|--------|-------------|
|
|
69
|
+
| Cursor | `~/.cursor/mcp.json` or `.cursor/mcp.json` |
|
|
70
|
+
| Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json` |
|
|
71
|
+
| Claude Code | `.mcp.json` at project root |
|
|
72
|
+
|
|
73
|
+
## Tools
|
|
74
|
+
|
|
75
|
+
| Tool | Purpose |
|
|
76
|
+
|------|---------|
|
|
77
|
+
| `joinquest_integration_get_agent_playbook` | **Start here** — end-to-end agent workflow (phases 1–8) |
|
|
78
|
+
| `joinquest_integration_get_integration_guide` | Full integration guide (markdown) |
|
|
79
|
+
| `joinquest_integration_get_discovery_prompt` | Agent interview script |
|
|
80
|
+
| `joinquest_integration_get_catalog_tag_taxonomy` | Valid tag IDs |
|
|
81
|
+
| `joinquest_integration_list_my_games` | Owner's games + visibility |
|
|
82
|
+
| `joinquest_integration_get_game_checks` | Checklist + metadata |
|
|
83
|
+
| `joinquest_integration_run_game_checks` | Run manifest / provision / JWT suite |
|
|
84
|
+
| `joinquest_integration_update_game_metadata` | Save catalog copy + tags |
|
|
85
|
+
| `joinquest_integration_get_game_credentials` | serviceToken + webhook secret |
|
|
86
|
+
| `joinquest_integration_get_example_provision_payload` | Sample provision JSON |
|
|
87
|
+
| `joinquest_integration_request_public_release` | Submit for catalog review |
|
|
88
|
+
|
|
89
|
+
## Run from repo
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
cd mcp/joinquest-integration
|
|
93
|
+
npm install
|
|
94
|
+
JOINQUEST_API_KEY=your-key node src/index.js
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Tests
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npm test
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Publishing (maintainers)
|
|
104
|
+
|
|
105
|
+
From repo root (requires `@joinquest` scope access + npm 2FA):
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
NPM_OTP=123456 ./scripts/publish-joinquest-mcp.sh
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Use a code from your authenticator app when prompted. For GitHub Actions, add a **granular access token** with **Publish** on `@joinquest/*` and **bypass 2FA** enabled as `NPM_TOKEN`.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Cursor injects ELECTRON_RUN_AS_NODE and a bundled node on PATH, which breaks stdio MCP.
|
|
3
|
+
set -eu
|
|
4
|
+
|
|
5
|
+
unset ELECTRON_RUN_AS_NODE
|
|
6
|
+
export PATH="/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:${PATH:-}"
|
|
7
|
+
|
|
8
|
+
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
9
|
+
NODE="${JOINQUEST_NODE:-$(command -v node)}"
|
|
10
|
+
|
|
11
|
+
exec "$NODE" "$DIR/../src/index.js"
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@joinquest/mcp-integration",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "JoinQuest integration MCP server for game developer agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"joinquest",
|
|
10
|
+
"mcp",
|
|
11
|
+
"model-context-protocol",
|
|
12
|
+
"game-development",
|
|
13
|
+
"integration"
|
|
14
|
+
],
|
|
15
|
+
"files": [
|
|
16
|
+
"bin",
|
|
17
|
+
"src",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/scruffyprodigy/playhub.git",
|
|
23
|
+
"directory": "mcp/joinquest-integration"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/scruffyprodigy/playhub/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://joinquest.cc/developers",
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"bin": {
|
|
33
|
+
"joinquest-integration-mcp": "src/index.js",
|
|
34
|
+
"joinquest-integration-mcp-cursor": "bin/joinquest-integration-mcp-cursor"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"start": "node src/index.js",
|
|
38
|
+
"test": "node --test test/*.test.js",
|
|
39
|
+
"prepublishOnly": "npm test"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=20"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
46
|
+
"zod": "^3.25.76"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const DEFAULT_JOINQUEST_API_URL = 'https://joinquest.cc/graphql'
|
|
2
|
+
|
|
3
|
+
export function loadConfig() {
|
|
4
|
+
const apiUrl = (process.env.JOINQUEST_API_URL || DEFAULT_JOINQUEST_API_URL).trim()
|
|
5
|
+
const apiKey = (process.env.JOINQUEST_API_KEY || '').trim()
|
|
6
|
+
const session = (process.env.JOINQUEST_SESSION || process.env.JOINQUEST_SESSION_COOKIE || '').trim()
|
|
7
|
+
const cookieName = (process.env.JOINQUEST_SESSION_COOKIE_NAME || 'lobby_session').trim()
|
|
8
|
+
|
|
9
|
+
if (apiKey) {
|
|
10
|
+
return { apiUrl, authHeader: `Bearer ${apiKey}` }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (session) {
|
|
14
|
+
const cookieValue = session.includes('=') ? session : `${cookieName}=${session}`
|
|
15
|
+
return { apiUrl, cookieValue }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
throw new Error(
|
|
19
|
+
'JOINQUEST_API_KEY is required. Generate one on your developer dashboard (Connect AI assistant), then set JOINQUEST_API_KEY in the MCP server env.',
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const CHECK_USER_ONE = 'a0000000-0000-4000-8000-000000000001'
|
|
2
|
+
const CHECK_USER_TWO = 'a0000000-0000-4000-8000-000000000002'
|
|
3
|
+
|
|
4
|
+
export function buildExampleProvisionPayload({ game, credentials, lobbyIssuer, lobbyReturnUrl, lobbyGraphqlUrl }) {
|
|
5
|
+
const mode = game?.modes?.[0]
|
|
6
|
+
if (!mode) {
|
|
7
|
+
throw new Error('Game has no synced modes — connect API and sync manifest first.')
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const seats = mode.seats ?? []
|
|
11
|
+
const seatCount = Math.min(2, seats.length)
|
|
12
|
+
if (seatCount === 0) {
|
|
13
|
+
throw new Error('Game mode has no expanded seats.')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const externalMatchId = 'example-match-' + game.id.slice(0, 8)
|
|
17
|
+
const assignmentSeats = seats.slice(0, seatCount).map((seat, index) => ({
|
|
18
|
+
seatKey: seat.seatKey,
|
|
19
|
+
lobbyUserId: index === 0 ? CHECK_USER_ONE : CHECK_USER_TWO,
|
|
20
|
+
displayName: index === 0 ? 'Player One' : 'Player Two',
|
|
21
|
+
}))
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
lobbyId: lobbyIssuer,
|
|
25
|
+
lobby: {
|
|
26
|
+
returnUrl: lobbyReturnUrl,
|
|
27
|
+
graphqlUrl: lobbyGraphqlUrl,
|
|
28
|
+
serviceToken: credentials.serviceToken,
|
|
29
|
+
},
|
|
30
|
+
assignment: {
|
|
31
|
+
externalMatchId,
|
|
32
|
+
gameMode: mode.modeKey,
|
|
33
|
+
seats: assignmentSeats,
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/graphql.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
export async function graphqlRequest(apiUrl, auth, query, variables = {}) {
|
|
2
|
+
const headers = {
|
|
3
|
+
'Content-Type': 'application/json',
|
|
4
|
+
}
|
|
5
|
+
if (auth.authHeader) {
|
|
6
|
+
headers.Authorization = auth.authHeader
|
|
7
|
+
} else if (auth.cookieValue) {
|
|
8
|
+
headers.Cookie = auth.cookieValue
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const response = await fetch(apiUrl, {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
headers,
|
|
14
|
+
body: JSON.stringify({ query, variables }),
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
let payload
|
|
18
|
+
try {
|
|
19
|
+
payload = await response.json()
|
|
20
|
+
} catch {
|
|
21
|
+
throw new Error(`JoinQuest API returned non-JSON (${response.status})`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
const detail = payload.errors?.[0]?.message || payload.error || response.statusText
|
|
26
|
+
throw new Error(detail || `API request failed (${response.status})`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (payload.errors?.length) {
|
|
30
|
+
throw new Error(payload.errors[0]?.message || 'GraphQL request failed')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return payload.data
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const QUERIES = {
|
|
37
|
+
agentPlaybook: `
|
|
38
|
+
query DeveloperAgentPlaybook {
|
|
39
|
+
developerAgentPlaybook
|
|
40
|
+
}
|
|
41
|
+
`,
|
|
42
|
+
integrationGuide: `
|
|
43
|
+
query DeveloperIntegrationGuide {
|
|
44
|
+
developerIntegrationGuide
|
|
45
|
+
}
|
|
46
|
+
`,
|
|
47
|
+
discoveryPrompt: `
|
|
48
|
+
query DeveloperDiscoveryPrompt {
|
|
49
|
+
developerDiscoveryPrompt
|
|
50
|
+
}
|
|
51
|
+
`,
|
|
52
|
+
catalogTagTaxonomy: `
|
|
53
|
+
query CatalogTagTaxonomy {
|
|
54
|
+
catalogTagTaxonomy {
|
|
55
|
+
id
|
|
56
|
+
label
|
|
57
|
+
description
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
`,
|
|
61
|
+
myGames: `
|
|
62
|
+
query MyGames {
|
|
63
|
+
myGames {
|
|
64
|
+
id
|
|
65
|
+
slug
|
|
66
|
+
name
|
|
67
|
+
shortDescription
|
|
68
|
+
visibility
|
|
69
|
+
apiBaseUrl
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
`,
|
|
73
|
+
myGame: `
|
|
74
|
+
query MyGame($id: ID!) {
|
|
75
|
+
myGame(id: $id) {
|
|
76
|
+
id
|
|
77
|
+
slug
|
|
78
|
+
name
|
|
79
|
+
shortDescription
|
|
80
|
+
longDescription
|
|
81
|
+
howToPlay
|
|
82
|
+
tags
|
|
83
|
+
visibility
|
|
84
|
+
apiBaseUrl
|
|
85
|
+
integrationChecks {
|
|
86
|
+
checkId
|
|
87
|
+
status
|
|
88
|
+
message
|
|
89
|
+
detail
|
|
90
|
+
ranAt
|
|
91
|
+
}
|
|
92
|
+
modes {
|
|
93
|
+
modeKey
|
|
94
|
+
displayName
|
|
95
|
+
minPlayers
|
|
96
|
+
maxPlayers
|
|
97
|
+
seats {
|
|
98
|
+
seatKey
|
|
99
|
+
queuePath
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
`,
|
|
105
|
+
myGameCredentials: `
|
|
106
|
+
query MyGameCredentials($id: ID!) {
|
|
107
|
+
myGameCredentials(id: $id) {
|
|
108
|
+
serviceToken
|
|
109
|
+
webhookSecret
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
`,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const MUTATIONS = {
|
|
116
|
+
runMyGameChecks: `
|
|
117
|
+
mutation RunMyGameChecks($gameId: ID!) {
|
|
118
|
+
runMyGameChecks(gameId: $gameId) {
|
|
119
|
+
checkId
|
|
120
|
+
status
|
|
121
|
+
message
|
|
122
|
+
detail
|
|
123
|
+
ranAt
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
`,
|
|
127
|
+
updateMyGameMetadata: `
|
|
128
|
+
mutation UpdateMyGameMetadata($input: UpdateMyGameMetadataInput!) {
|
|
129
|
+
updateMyGameMetadata(input: $input) {
|
|
130
|
+
id
|
|
131
|
+
shortDescription
|
|
132
|
+
longDescription
|
|
133
|
+
howToPlay
|
|
134
|
+
tags
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
`,
|
|
138
|
+
requestPublicRelease: `
|
|
139
|
+
mutation RequestPublicRelease($gameId: ID!) {
|
|
140
|
+
requestPublicRelease(gameId: $gameId) {
|
|
141
|
+
id
|
|
142
|
+
visibility
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
`,
|
|
146
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
5
|
+
import { loadConfig } from './config.js'
|
|
6
|
+
import { registerJoinQuestIntegrationTools } from './tools.js'
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
const config = loadConfig()
|
|
10
|
+
const server = new McpServer({
|
|
11
|
+
name: 'joinquest-integration',
|
|
12
|
+
version: '0.1.0',
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
registerJoinQuestIntegrationTools(server, config)
|
|
16
|
+
|
|
17
|
+
const transport = new StdioServerTransport()
|
|
18
|
+
await server.connect(transport)
|
|
19
|
+
console.error('JoinQuest integration MCP server running (stdio)')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
main().catch((error) => {
|
|
23
|
+
console.error('JoinQuest integration MCP fatal error:', error.message)
|
|
24
|
+
process.exit(1)
|
|
25
|
+
})
|
package/src/tools.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import {
|
|
3
|
+
MUTATIONS,
|
|
4
|
+
QUERIES,
|
|
5
|
+
graphqlRequest,
|
|
6
|
+
} from './graphql.js'
|
|
7
|
+
import { buildExampleProvisionPayload } from './example-provision.js'
|
|
8
|
+
|
|
9
|
+
function textResult(value) {
|
|
10
|
+
const text = typeof value === 'string' ? value : JSON.stringify(value, null, 2)
|
|
11
|
+
return { content: [{ type: 'text', text }] }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function lobbyUrls(apiUrl) {
|
|
15
|
+
const origin = apiUrl.replace(/\/graphql\/?$/, '')
|
|
16
|
+
const issuer = (process.env.JOINQUEST_ISSUER_URL || origin).replace(/\/$/, '')
|
|
17
|
+
let publicUrl = process.env.JOINQUEST_PUBLIC_URL || origin
|
|
18
|
+
if (origin.includes('localhost:8080') && !process.env.JOINQUEST_PUBLIC_URL) {
|
|
19
|
+
publicUrl = 'http://localhost:5173'
|
|
20
|
+
}
|
|
21
|
+
publicUrl = publicUrl.replace(/\/$/, '')
|
|
22
|
+
return {
|
|
23
|
+
lobbyIssuer: issuer,
|
|
24
|
+
lobbyReturnUrl: `${publicUrl}/return`,
|
|
25
|
+
lobbyGraphqlUrl: `${issuer}/graphql`,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function registerJoinQuestIntegrationTools(server, config) {
|
|
30
|
+
const { apiUrl, authHeader, cookieValue } = config
|
|
31
|
+
const auth = authHeader ? { authHeader } : { cookieValue }
|
|
32
|
+
const gql = (query, variables) => graphqlRequest(apiUrl, auth, query, variables)
|
|
33
|
+
|
|
34
|
+
server.registerTool(
|
|
35
|
+
'joinquest_integration_get_agent_playbook',
|
|
36
|
+
{
|
|
37
|
+
description:
|
|
38
|
+
'Returns the end-to-end agent workflow for integrating a game with JoinQuest (phases 1–8: discovery, MCP setup, API implementation, registration, checks, metadata, test, release). Start here for vibe-coding integrations.',
|
|
39
|
+
inputSchema: z.object({}),
|
|
40
|
+
},
|
|
41
|
+
async () => {
|
|
42
|
+
const data = await gql(QUERIES.agentPlaybook)
|
|
43
|
+
return textResult(data.developerAgentPlaybook)
|
|
44
|
+
},
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
server.registerTool(
|
|
48
|
+
'joinquest_integration_get_integration_guide',
|
|
49
|
+
{
|
|
50
|
+
description: 'Returns the full JoinQuest developer integration guide (markdown).',
|
|
51
|
+
inputSchema: z.object({}),
|
|
52
|
+
},
|
|
53
|
+
async () => {
|
|
54
|
+
const data = await gql(QUERIES.integrationGuide)
|
|
55
|
+
return textResult(data.developerIntegrationGuide)
|
|
56
|
+
},
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
server.registerTool(
|
|
60
|
+
'joinquest_integration_get_discovery_prompt',
|
|
61
|
+
{
|
|
62
|
+
description: 'Returns the agent discovery interview script for understanding a game before drafting catalog copy or seatTemplate guidance.',
|
|
63
|
+
inputSchema: z.object({}),
|
|
64
|
+
},
|
|
65
|
+
async () => {
|
|
66
|
+
const data = await gql(QUERIES.discoveryPrompt)
|
|
67
|
+
return textResult(data.developerDiscoveryPrompt)
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
server.registerTool(
|
|
72
|
+
'joinquest_integration_get_catalog_tag_taxonomy',
|
|
73
|
+
{
|
|
74
|
+
description: 'Returns valid catalog tag IDs and labels for updateMyGameMetadata.',
|
|
75
|
+
inputSchema: z.object({}),
|
|
76
|
+
},
|
|
77
|
+
async () => {
|
|
78
|
+
const data = await gql(QUERIES.catalogTagTaxonomy)
|
|
79
|
+
return textResult(data.catalogTagTaxonomy)
|
|
80
|
+
},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
server.registerTool(
|
|
84
|
+
'joinquest_integration_list_my_games',
|
|
85
|
+
{
|
|
86
|
+
description: "List games owned by the signed-in developer, including visibility state.",
|
|
87
|
+
inputSchema: z.object({}),
|
|
88
|
+
},
|
|
89
|
+
async () => {
|
|
90
|
+
const data = await gql(QUERIES.myGames)
|
|
91
|
+
return textResult(data.myGames ?? [])
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
server.registerTool(
|
|
96
|
+
'joinquest_integration_get_game_checks',
|
|
97
|
+
{
|
|
98
|
+
description: 'Fetch latest integration checklist results and catalog metadata for one owned game.',
|
|
99
|
+
inputSchema: z.object({
|
|
100
|
+
gameId: z.string().describe('JoinQuest game UUID'),
|
|
101
|
+
}),
|
|
102
|
+
},
|
|
103
|
+
async ({ gameId }) => {
|
|
104
|
+
const data = await gql(QUERIES.myGame, { id: gameId })
|
|
105
|
+
if (!data.myGame) {
|
|
106
|
+
throw new Error('Game not found or not owned by this session.')
|
|
107
|
+
}
|
|
108
|
+
return textResult(data.myGame)
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
server.registerTool(
|
|
113
|
+
'joinquest_integration_run_game_checks',
|
|
114
|
+
{
|
|
115
|
+
description: 'Run manifest, provision, and JWT integration checks for an owned game.',
|
|
116
|
+
inputSchema: z.object({
|
|
117
|
+
gameId: z.string().describe('JoinQuest game UUID'),
|
|
118
|
+
}),
|
|
119
|
+
},
|
|
120
|
+
async ({ gameId }) => {
|
|
121
|
+
const data = await gql(MUTATIONS.runMyGameChecks, { gameId })
|
|
122
|
+
return textResult(data.runMyGameChecks ?? [])
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
server.registerTool(
|
|
127
|
+
'joinquest_integration_update_game_metadata',
|
|
128
|
+
{
|
|
129
|
+
description: 'Save catalog listing copy and tags after developer approval. Use catalogTagTaxonomy IDs.',
|
|
130
|
+
inputSchema: z.object({
|
|
131
|
+
gameId: z.string(),
|
|
132
|
+
shortDescription: z.string().optional(),
|
|
133
|
+
longDescription: z.string().optional(),
|
|
134
|
+
howToPlay: z.string().optional(),
|
|
135
|
+
tags: z.array(z.string()).optional(),
|
|
136
|
+
}),
|
|
137
|
+
},
|
|
138
|
+
async ({ gameId, shortDescription, longDescription, howToPlay, tags }) => {
|
|
139
|
+
const input = { gameId }
|
|
140
|
+
if (shortDescription !== undefined) input.shortDescription = shortDescription
|
|
141
|
+
if (longDescription !== undefined) input.longDescription = longDescription
|
|
142
|
+
if (howToPlay !== undefined) input.howToPlay = howToPlay
|
|
143
|
+
if (tags !== undefined) input.tags = tags
|
|
144
|
+
|
|
145
|
+
const data = await gql(MUTATIONS.updateMyGameMetadata, { input })
|
|
146
|
+
return textResult(data.updateMyGameMetadata)
|
|
147
|
+
},
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
server.registerTool(
|
|
151
|
+
'joinquest_integration_get_game_credentials',
|
|
152
|
+
{
|
|
153
|
+
description: 'Return serviceToken and webhookSecret for an owned game (sensitive).',
|
|
154
|
+
inputSchema: z.object({
|
|
155
|
+
gameId: z.string(),
|
|
156
|
+
}),
|
|
157
|
+
},
|
|
158
|
+
async ({ gameId }) => {
|
|
159
|
+
const data = await gql(QUERIES.myGameCredentials, { id: gameId })
|
|
160
|
+
if (!data.myGameCredentials) {
|
|
161
|
+
throw new Error('Credentials not found or game not owned by this session.')
|
|
162
|
+
}
|
|
163
|
+
return textResult(data.myGameCredentials)
|
|
164
|
+
},
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
server.registerTool(
|
|
168
|
+
'joinquest_integration_get_example_provision_payload',
|
|
169
|
+
{
|
|
170
|
+
description: 'Build a sample POST /api/v1/matches body for local testing, using synced modes and credentials.',
|
|
171
|
+
inputSchema: z.object({
|
|
172
|
+
gameId: z.string(),
|
|
173
|
+
}),
|
|
174
|
+
},
|
|
175
|
+
async ({ gameId }) => {
|
|
176
|
+
const [gameData, credData] = await Promise.all([
|
|
177
|
+
gql(QUERIES.myGame, { id: gameId }),
|
|
178
|
+
gql(QUERIES.myGameCredentials, { id: gameId }),
|
|
179
|
+
])
|
|
180
|
+
if (!gameData.myGame || !credData.myGameCredentials) {
|
|
181
|
+
throw new Error('Game or credentials not found.')
|
|
182
|
+
}
|
|
183
|
+
const urls = lobbyUrls(apiUrl)
|
|
184
|
+
const payload = buildExampleProvisionPayload({
|
|
185
|
+
game: gameData.myGame,
|
|
186
|
+
credentials: credData.myGameCredentials,
|
|
187
|
+
...urls,
|
|
188
|
+
})
|
|
189
|
+
return textResult(payload)
|
|
190
|
+
},
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
server.registerTool(
|
|
194
|
+
'joinquest_integration_request_public_release',
|
|
195
|
+
{
|
|
196
|
+
description: 'Submit an owned game for public catalog review when checklist and metadata gates pass.',
|
|
197
|
+
inputSchema: z.object({
|
|
198
|
+
gameId: z.string(),
|
|
199
|
+
}),
|
|
200
|
+
},
|
|
201
|
+
async ({ gameId }) => {
|
|
202
|
+
const data = await gql(MUTATIONS.requestPublicRelease, { gameId })
|
|
203
|
+
return textResult(data.requestPublicRelease)
|
|
204
|
+
},
|
|
205
|
+
)
|
|
206
|
+
}
|