agent-planner-mcp 0.5.0 → 0.6.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/src/api-client.js CHANGED
@@ -208,6 +208,21 @@ const nodes = {
208
208
  */
209
209
  deleteNode: async (planId, nodeId) => {
210
210
  await apiClient.delete(`/plans/${planId}/nodes/${nodeId}`);
211
+ },
212
+
213
+ claimTask: async (planId, nodeId, agentId = 'mcp-agent', ttlMinutes = 30) => {
214
+ const response = await apiClient.post(`/plans/${planId}/nodes/${nodeId}/claim`, { agent_id: agentId, ttl_minutes: ttlMinutes });
215
+ return response.data;
216
+ },
217
+
218
+ releaseTask: async (planId, nodeId, agentId = 'mcp-agent') => {
219
+ const response = await apiClient.delete(`/plans/${planId}/nodes/${nodeId}/claim`, { data: { agent_id: agentId } });
220
+ return response.data;
221
+ },
222
+
223
+ getTaskClaim: async (planId, nodeId) => {
224
+ const response = await apiClient.get(`/plans/${planId}/nodes/${nodeId}/claim`);
225
+ return response.data;
211
226
  }
212
227
  };
213
228
 
@@ -555,89 +570,47 @@ const goals = {
555
570
  unlinkPlan: async (goalId, planId) => {
556
571
  const response = await apiClient.delete(`/goals/${goalId}/plans/${planId}`);
557
572
  return response.data;
558
- }
559
- };
573
+ },
560
574
 
561
- /**
562
- * Knowledge Store API functions
563
- */
564
- const knowledge = {
565
- /**
566
- * List knowledge entries with optional filters
567
- * GET /knowledge
568
- */
569
- listEntries: async (storeIdOrFilters, filters = {}) => {
570
- const params = new URLSearchParams();
571
- // Support both (storeId, filters) and (filters) calling patterns
572
- if (typeof storeIdOrFilters === 'string') {
573
- params.append('scopeId', storeIdOrFilters);
574
- if (filters.entry_type) params.append('entryType', filters.entry_type);
575
- if (filters.tags) params.append('tags', filters.tags);
576
- if (filters.limit) params.append('limit', filters.limit);
577
- if (filters.offset) params.append('offset', filters.offset);
578
- } else if (typeof storeIdOrFilters === 'object') {
579
- const f = storeIdOrFilters;
580
- if (f.scope) params.append('scope', f.scope);
581
- if (f.scope_id) params.append('scopeId', f.scope_id);
582
- if (f.entry_type) params.append('entryType', f.entry_type);
583
- if (f.limit) params.append('limit', f.limit);
584
- if (f.offset) params.append('offset', f.offset);
585
- }
586
- const response = await apiClient.get(`/knowledge?${params.toString()}`);
575
+ // v2 goal-dependency endpoints
576
+ getPath: async (goalId, maxDepth) => {
577
+ const params = maxDepth ? `?max_depth=${maxDepth}` : '';
578
+ const response = await apiClient.get(`/goals/v2/${goalId}/path${params}`);
587
579
  return response.data;
588
580
  },
589
581
 
590
- /**
591
- * Alias for listEntries — used by get_context and understand_context tools
592
- */
593
- getEntries: async (filters = {}) => {
594
- return knowledge.listEntries(filters);
582
+ getProgress: async (goalId) => {
583
+ const response = await apiClient.get(`/goals/v2/${goalId}/progress`);
584
+ return response.data;
595
585
  },
596
586
 
597
- /**
598
- * Get a single knowledge entry
599
- * GET /knowledge/:id
600
- */
601
- getEntry: async (entryId) => {
602
- const response = await apiClient.get(`/knowledge/${entryId}`);
587
+ listAchievers: async (goalId) => {
588
+ const response = await apiClient.get(`/goals/v2/${goalId}/achievers`);
603
589
  return response.data;
604
590
  },
605
591
 
606
- /**
607
- * Create a knowledge entry
608
- * POST /knowledge
609
- */
610
- createEntry: async (data) => {
611
- const response = await apiClient.post('/knowledge', data);
592
+ addAchiever: async (goalId, sourceNodeId, weight) => {
593
+ const response = await apiClient.post(`/goals/v2/${goalId}/achievers`, {
594
+ source_node_id: sourceNodeId,
595
+ weight: weight ?? 1,
596
+ });
612
597
  return response.data;
613
598
  },
614
599
 
615
- /**
616
- * Update a knowledge entry
617
- * PUT /knowledge/:id
618
- */
619
- updateEntry: async (entryId, data) => {
620
- const response = await apiClient.put(`/knowledge/${entryId}`, data);
600
+ removeAchiever: async (goalId, depId) => {
601
+ const response = await apiClient.delete(`/goals/v2/${goalId}/achievers/${depId}`);
621
602
  return response.data;
622
603
  },
623
604
 
624
- /**
625
- * Delete a knowledge entry
626
- * DELETE /knowledge/:id
627
- */
628
- deleteEntry: async (entryId) => {
629
- const response = await apiClient.delete(`/knowledge/${entryId}`);
605
+ getKnowledgeGaps: async (goalId) => {
606
+ const response = await apiClient.get(`/goals/v2/${goalId}/knowledge-gaps`);
630
607
  return response.data;
631
608
  },
632
609
 
633
- /**
634
- * Semantic search across knowledge entries
635
- * POST /knowledge/search
636
- */
637
- search: async (data) => {
638
- const response = await apiClient.post('/knowledge/search', data);
610
+ getDashboard: async () => {
611
+ const response = await apiClient.get('/goals/v2/dashboard');
639
612
  return response.data;
640
- }
613
+ },
641
614
  };
642
615
 
643
616
  /**
@@ -679,6 +652,263 @@ const context = {
679
652
  }
680
653
  };
681
654
 
655
+ /**
656
+ * Graphiti Knowledge Graph API functions (proxied through AgentPlanner API)
657
+ */
658
+ const graphiti = {
659
+ /**
660
+ * Get Graphiti status
661
+ * GET /knowledge/graphiti/status
662
+ */
663
+ getStatus: async () => {
664
+ const response = await apiClient.get('/knowledge/graphiti/status');
665
+ return response.data;
666
+ },
667
+
668
+ /**
669
+ * Add a knowledge episode to Graphiti
670
+ * POST /knowledge/episodes
671
+ */
672
+ addEpisode: async (data) => {
673
+ const response = await apiClient.post('/knowledge/episodes', data);
674
+ return response.data;
675
+ },
676
+
677
+ /**
678
+ * Search knowledge in Graphiti temporal graph
679
+ * POST /knowledge/graph-search
680
+ */
681
+ graphSearch: async (data) => {
682
+ const response = await apiClient.post('/knowledge/graph-search', data);
683
+ return response.data;
684
+ },
685
+
686
+ /**
687
+ * Search entities in Graphiti
688
+ * POST /knowledge/entities
689
+ */
690
+ searchEntities: async (data) => {
691
+ const response = await apiClient.post('/knowledge/entities', data);
692
+ return response.data;
693
+ },
694
+
695
+ /**
696
+ * Detect contradictions in knowledge
697
+ * POST /knowledge/contradictions
698
+ */
699
+ detectContradictions: async (data) => {
700
+ const response = await apiClient.post('/knowledge/contradictions', data);
701
+ return response.data;
702
+ },
703
+
704
+ /**
705
+ * Get recent episodes from Graphiti
706
+ * GET /knowledge/episodes
707
+ */
708
+ getEpisodes: async ({ max_episodes = 20 } = {}) => {
709
+ const response = await apiClient.get('/knowledge/episodes', { params: { max_episodes } });
710
+ return response.data;
711
+ },
712
+
713
+ /**
714
+ * Delete an episode from Graphiti
715
+ * DELETE /knowledge/episodes/:episodeId
716
+ */
717
+ deleteEpisode: async (episodeId) => {
718
+ const response = await apiClient.delete(`/knowledge/episodes/${episodeId}`);
719
+ return response.data;
720
+ }
721
+ };
722
+
723
+ // ─── Dependencies (cross-plan & external) ─────────────────────
724
+ const dependencies = {
725
+ /**
726
+ * Create a cross-plan dependency edge
727
+ * POST /dependencies/cross-plan
728
+ */
729
+ createCrossPlan: async (data) => {
730
+ const response = await apiClient.post('/dependencies/cross-plan', data);
731
+ return response.data;
732
+ },
733
+
734
+ /**
735
+ * List cross-plan dependency edges between plans
736
+ * GET /dependencies/cross-plan?plan_ids=id1,id2
737
+ */
738
+ listCrossPlan: async (planIds) => {
739
+ const response = await apiClient.get('/dependencies/cross-plan', {
740
+ params: { plan_ids: planIds.join(',') },
741
+ });
742
+ return response.data;
743
+ },
744
+
745
+ /**
746
+ * Create an external dependency node (and optionally a blocking edge)
747
+ * POST /dependencies/external
748
+ */
749
+ createExternal: async (data) => {
750
+ const response = await apiClient.post('/dependencies/external', data);
751
+ return response.data;
752
+ },
753
+ };
754
+
755
+ /**
756
+ * Create an API client bound to a specific token.
757
+ * Used by the HTTP MCP server to create per-session clients.
758
+ * @param {string} token - API token or JWT
759
+ * @returns {Object} - API client modules (plans, nodes, etc.)
760
+ */
761
+ function createApiClient(token) {
762
+ const scheme = getAuthScheme(token);
763
+ const client = axios.create({
764
+ baseURL: process.env.API_URL || 'http://localhost:3000',
765
+ headers: {
766
+ 'Content-Type': 'application/json',
767
+ 'Authorization': token ? `${scheme} ${token}` : undefined
768
+ }
769
+ });
770
+
771
+ // Reuse the same interceptors
772
+ if (process.env.NODE_ENV === 'development') {
773
+ client.interceptors.request.use(request => {
774
+ console.error(`API Request: ${request.method.toUpperCase()} ${request.url}`);
775
+ return request;
776
+ });
777
+ }
778
+ client.interceptors.response.use(
779
+ response => response,
780
+ error => {
781
+ if (error.response && error.response.status === 401) {
782
+ console.error('API Error: Authentication failed (401).');
783
+ }
784
+ return Promise.reject(error);
785
+ }
786
+ );
787
+
788
+ // Build the same module structure using the per-session client
789
+ return {
790
+ plans: {
791
+ getPlans: async () => (await client.get('/plans')).data,
792
+ getPlan: async (planId) => (await client.get(`/plans/${planId}`)).data,
793
+ createPlan: async (planData) => (await client.post('/plans', planData)).data,
794
+ updatePlan: async (planId, planData) => (await client.put(`/plans/${planId}`, planData)).data,
795
+ deletePlan: async (planId) => await client.delete(`/plans/${planId}`),
796
+ updateVisibility: async (planId, data) => (await client.put(`/plans/${planId}/visibility`, data)).data,
797
+ getPublicPlan: async (planId) => (await client.get(`/plans/${planId}/public`)).data,
798
+ },
799
+ nodes: {
800
+ getNodes: async (planId, options = {}) => {
801
+ const params = new URLSearchParams();
802
+ if (options.include_details) params.append('include_details', 'true');
803
+ const qs = params.toString() ? `?${params.toString()}` : '';
804
+ return (await client.get(`/plans/${planId}/nodes${qs}`)).data;
805
+ },
806
+ getNode: async (planId, nodeId) => (await client.get(`/plans/${planId}/nodes/${nodeId}`)).data,
807
+ createNode: async (planId, nodeData) => (await client.post(`/plans/${planId}/nodes`, nodeData)).data,
808
+ updateNode: async (planId, nodeId, nodeData) => (await client.put(`/plans/${planId}/nodes/${nodeId}`, nodeData)).data,
809
+ updateNodeStatus: async (planId, nodeId, status) => (await client.put(`/plans/${planId}/nodes/${nodeId}/status`, { status })).data,
810
+ deleteNode: async (planId, nodeId) => await client.delete(`/plans/${planId}/nodes/${nodeId}`),
811
+ claimTask: async (planId, nodeId, agentId = 'mcp-agent', ttlMinutes = 30) => (await client.post(`/plans/${planId}/nodes/${nodeId}/claim`, { agent_id: agentId, ttl_minutes: ttlMinutes })).data,
812
+ releaseTask: async (planId, nodeId, agentId = 'mcp-agent') => (await client.delete(`/plans/${planId}/nodes/${nodeId}/claim`, { data: { agent_id: agentId } })).data,
813
+ getTaskClaim: async (planId, nodeId) => (await client.get(`/plans/${planId}/nodes/${nodeId}/claim`)).data,
814
+ },
815
+ comments: {
816
+ getComments: async (planId, nodeId) => (await client.get(`/plans/${planId}/nodes/${nodeId}/comments`)).data,
817
+ addComment: async (planId, nodeId, data) => (await client.post(`/plans/${planId}/nodes/${nodeId}/comments`, data)).data,
818
+ },
819
+ logs: {
820
+ getLogs: async (planId, nodeId) => (await client.get(`/plans/${planId}/nodes/${nodeId}/logs`)).data,
821
+ addLogEntry: async (planId, nodeId, data) => (await client.post(`/plans/${planId}/nodes/${nodeId}/log`, data)).data,
822
+ },
823
+ activity: {
824
+ getPlanActivity: async (planId) => (await client.get(`/activity/plans/${planId}/activity`)).data,
825
+ getGlobalActivity: async () => (await client.get('/activity/feed')).data,
826
+ },
827
+ search: {
828
+ searchPlan: async (planId, query) => {
829
+ try {
830
+ return (await client.get(`/search/plan/${planId}`, { params: { query } })).data;
831
+ } catch (error) {
832
+ return { results: [], count: 0, query };
833
+ }
834
+ },
835
+ globalSearch: async (query) => {
836
+ try {
837
+ return (await client.get('/search', { params: { query } })).data;
838
+ } catch (error) {
839
+ return { results: [], count: 0, query };
840
+ }
841
+ },
842
+ },
843
+ tokens: {
844
+ getTokens: async () => (await client.get('/auth/token')).data,
845
+ createToken: async (data) => (await client.post('/auth/token', data)).data,
846
+ revokeToken: async (tokenId) => await client.delete(`/auth/token/${tokenId}`),
847
+ },
848
+ organizations: {
849
+ list: async () => { const r = await client.get('/organizations'); return r.data.organizations || r.data; },
850
+ get: async (orgId) => (await client.get(`/organizations/${orgId}`)).data,
851
+ create: async (data) => (await client.post('/organizations', data)).data,
852
+ update: async (orgId, data) => (await client.put(`/organizations/${orgId}`, data)).data,
853
+ delete: async (orgId) => (await client.delete(`/organizations/${orgId}`)).data,
854
+ listMembers: async (orgId) => { const r = await client.get(`/organizations/${orgId}/members`); return r.data.members || r.data; },
855
+ addMember: async (orgId, data) => (await client.post(`/organizations/${orgId}/members`, data)).data,
856
+ removeMember: async (orgId, memberId) => (await client.delete(`/organizations/${orgId}/members/${memberId}`)).data,
857
+ },
858
+ goals: {
859
+ list: async (filters = {}) => {
860
+ const params = new URLSearchParams();
861
+ if (filters.organization_id) params.append('organization_id', filters.organization_id);
862
+ if (filters.status) params.append('status', filters.status);
863
+ const r = await client.get(`/goals?${params.toString()}`);
864
+ return r.data.goals || r.data;
865
+ },
866
+ get: async (goalId) => (await client.get(`/goals/${goalId}`)).data,
867
+ create: async (data) => (await client.post('/goals', data)).data,
868
+ update: async (goalId, data) => (await client.put(`/goals/${goalId}`, data)).data,
869
+ updateMetrics: async (goalId, metrics) => (await client.put(`/goals/${goalId}/metrics`, { metrics })).data,
870
+ delete: async (goalId) => (await client.delete(`/goals/${goalId}`)).data,
871
+ linkPlan: async (goalId, planId) => (await client.post(`/goals/${goalId}/plans/${planId}`)).data,
872
+ unlinkPlan: async (goalId, planId) => (await client.delete(`/goals/${goalId}/plans/${planId}`)).data,
873
+ getPath: async (goalId, maxDepth) => { const p = maxDepth ? `?max_depth=${maxDepth}` : ''; return (await client.get(`/goals/v2/${goalId}/path${p}`)).data; },
874
+ getProgress: async (goalId) => (await client.get(`/goals/v2/${goalId}/progress`)).data,
875
+ listAchievers: async (goalId) => (await client.get(`/goals/v2/${goalId}/achievers`)).data,
876
+ addAchiever: async (goalId, sourceNodeId, weight) => (await client.post(`/goals/v2/${goalId}/achievers`, { source_node_id: sourceNodeId, weight: weight ?? 1 })).data,
877
+ removeAchiever: async (goalId, depId) => (await client.delete(`/goals/v2/${goalId}/achievers/${depId}`)).data,
878
+ getKnowledgeGaps: async (goalId) => (await client.get(`/goals/v2/${goalId}/knowledge-gaps`)).data,
879
+ getDashboard: async () => (await client.get('/goals/v2/dashboard')).data,
880
+ },
881
+ context: {
882
+ getNodeContext: async (nodeId, options = {}) => {
883
+ const params = new URLSearchParams({ node_id: nodeId });
884
+ if (options.include_knowledge !== undefined) params.append('include_knowledge', options.include_knowledge);
885
+ if (options.include_siblings !== undefined) params.append('include_siblings', options.include_siblings);
886
+ return (await client.get(`/context?${params.toString()}`)).data;
887
+ },
888
+ getPlanContext: async (planId, options = {}) => {
889
+ const params = new URLSearchParams({ plan_id: planId });
890
+ if (options.include_knowledge !== undefined) params.append('include_knowledge', options.include_knowledge);
891
+ return (await client.get(`/context/plan?${params.toString()}`)).data;
892
+ },
893
+ },
894
+ graphiti: {
895
+ getStatus: async () => (await client.get('/knowledge/graphiti/status')).data,
896
+ addEpisode: async (data) => (await client.post('/knowledge/episodes', data)).data,
897
+ graphSearch: async (data) => (await client.post('/knowledge/graph-search', data)).data,
898
+ searchEntities: async (data) => (await client.post('/knowledge/entities', data)).data,
899
+ detectContradictions: async (data) => (await client.post('/knowledge/contradictions', data)).data,
900
+ getEpisodes: async ({ max_episodes = 20 } = {}) => (await client.get('/knowledge/episodes', { params: { max_episodes } })).data,
901
+ deleteEpisode: async (episodeId) => (await client.delete(`/knowledge/episodes/${episodeId}`)).data,
902
+ },
903
+ dependencies: {
904
+ createCrossPlan: async (data) => (await client.post('/dependencies/cross-plan', data)).data,
905
+ listCrossPlan: async (planIds) => (await client.get('/dependencies/cross-plan', { params: { plan_ids: planIds.join(',') } })).data,
906
+ createExternal: async (data) => (await client.post('/dependencies/external', data)).data,
907
+ },
908
+ axiosInstance: client,
909
+ };
910
+ }
911
+
682
912
  // Export API client functions
683
913
  // Export the axios instance for direct use
684
914
  const axiosInstance = apiClient;
@@ -693,7 +923,9 @@ module.exports = {
693
923
  tokens,
694
924
  organizations,
695
925
  goals,
696
- knowledge,
697
926
  context,
698
- axiosInstance // Export for direct API calls
927
+ graphiti,
928
+ dependencies,
929
+ axiosInstance, // Export for direct API calls
930
+ createApiClient // Factory for per-session clients (HTTP mode)
699
931
  };
package/src/index.js CHANGED
@@ -3,6 +3,7 @@ const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
3
3
  const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
4
4
  const { MCPHTTPServer } = require('./server-http');
5
5
  const { setupTools } = require('./tools');
6
+ const { version } = require('../package.json');
6
7
  require('dotenv').config();
7
8
 
8
9
  /**
@@ -39,7 +40,7 @@ async function main() {
39
40
  const userApiToken = process.env.USER_API_TOKEN || process.env.API_TOKEN;
40
41
  console.error(`User API Token: ${userApiToken ? '***' + userApiToken.slice(-4) : 'NOT SET'}`);
41
42
  console.error(`MCP Server Name: ${process.env.MCP_SERVER_NAME || 'planning-system-mcp'}`);
42
- console.error(`MCP Server Version: ${process.env.MCP_SERVER_VERSION || '0.3.1'}`);
43
+ console.error(`MCP Server Version: ${process.env.MCP_SERVER_VERSION || version}`);
43
44
 
44
45
  // Validate required environment variables
45
46
  if (!userApiToken) {
@@ -71,7 +72,7 @@ async function main() {
71
72
  // Stdio transport mode (default)
72
73
  const server = new Server({
73
74
  name: process.env.MCP_SERVER_NAME || "planning-system-mcp",
74
- version: process.env.MCP_SERVER_VERSION || "0.3.1"
75
+ version: process.env.MCP_SERVER_VERSION || version
75
76
  }, {
76
77
  capabilities: {
77
78
  tools: {}
@@ -15,7 +15,9 @@
15
15
  const express = require('express');
16
16
  const { SessionManager } = require('./session-manager');
17
17
  const { setupTools } = require('./tools');
18
+ const { createApiClient } = require('./api-client');
18
19
  const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
20
+ const { version } = require('../package.json');
19
21
  require('dotenv').config();
20
22
 
21
23
  // MCP Protocol Version
@@ -60,8 +62,8 @@ class MCPHTTPServer {
60
62
 
61
63
  // Protocol version validation
62
64
  this.app.use((req, res, next) => {
63
- // Skip version check for health endpoint
64
- if (req.path === '/health') {
65
+ // Skip version check for health and discovery endpoints
66
+ if (req.path === '/health' || req.path === '/.well-known/mcp.json') {
65
67
  return next();
66
68
  }
67
69
 
@@ -77,26 +79,55 @@ class MCPHTTPServer {
77
79
  next();
78
80
  });
79
81
 
82
+ // Authentication — require Authorization header on /mcp
83
+ this.app.use((req, res, next) => {
84
+ if (req.path === '/health' || req.path === '/.well-known/mcp.json') return next();
85
+
86
+ const authHeader = req.get('Authorization');
87
+ if (!authHeader) {
88
+ return res.status(401).json({
89
+ jsonrpc: '2.0',
90
+ error: { code: -32000, message: 'Authorization header required. Use "Authorization: Bearer <token>" or "Authorization: ApiKey <token>".' }
91
+ });
92
+ }
93
+
94
+ const parts = authHeader.split(' ');
95
+ if (parts.length !== 2 || !['Bearer', 'ApiKey'].includes(parts[0])) {
96
+ return res.status(401).json({
97
+ jsonrpc: '2.0',
98
+ error: { code: -32000, message: 'Invalid Authorization format. Use "Bearer <token>" or "ApiKey <token>".' }
99
+ });
100
+ }
101
+
102
+ // Store the raw token for per-session API client creation
103
+ req.userToken = parts[1];
104
+ next();
105
+ });
106
+
80
107
  // Origin validation for security (DNS rebinding protection)
81
108
  this.app.use((req, res, next) => {
82
- // Skip origin check for health endpoint
83
- if (req.path === '/health') {
109
+ // Skip origin check for health and discovery endpoints
110
+ if (req.path === '/health' || req.path === '/.well-known/mcp.json') {
84
111
  return next();
85
112
  }
86
113
 
114
+ // Skip origin check when running on 0.0.0.0 (production container behind nginx)
115
+ // Origin validation is DNS rebinding protection, not relevant for server-to-server MCP
116
+ if (this.host === '0.0.0.0') return next();
117
+
87
118
  const origin = req.get('Origin');
88
119
 
89
120
  // If Origin header is present, validate it
90
121
  if (origin) {
91
- // For localhost binding, accept localhost origins
122
+ // Accept localhost and production origins
92
123
  const allowedOrigins = [
93
124
  'http://localhost',
94
125
  'http://127.0.0.1',
95
126
  `http://localhost:${this.port}`,
96
- `http://127.0.0.1:${this.port}`
127
+ `http://127.0.0.1:${this.port}`,
128
+ 'https://agentplanner.io'
97
129
  ];
98
130
 
99
- const originUrl = new URL(origin);
100
131
  const isAllowed = allowedOrigins.some(allowed => origin.startsWith(allowed));
101
132
 
102
133
  if (!isAllowed) {
@@ -119,6 +150,25 @@ class MCPHTTPServer {
119
150
  * Setup Express routes
120
151
  */
121
152
  setupRoutes() {
153
+ // MCP discovery endpoint (no auth required)
154
+ this.app.get('/.well-known/mcp.json', (req, res) => {
155
+ res.json({
156
+ mcp_version: '2025-03-26',
157
+ server: {
158
+ name: 'agent-planner-mcp',
159
+ version,
160
+ description: 'AI agent orchestration with planning, dependencies, knowledge graphs, and human oversight'
161
+ },
162
+ endpoints: { mcp: '/mcp' },
163
+ authentication: {
164
+ type: 'api_key',
165
+ header: 'Authorization',
166
+ format: 'ApiKey <token>'
167
+ },
168
+ capabilities: { tools: true }
169
+ });
170
+ });
171
+
122
172
  // Health check endpoint
123
173
  this.app.get('/health', (req, res) => {
124
174
  const stats = this.sessionManager.getStats();
@@ -127,7 +177,7 @@ class MCPHTTPServer {
127
177
  version: MCP_PROTOCOL_VERSION,
128
178
  server: {
129
179
  name: process.env.MCP_SERVER_NAME || 'planning-tools',
130
- version: process.env.MCP_SERVER_VERSION || '0.3.1'
180
+ version: process.env.MCP_SERVER_VERSION || version
131
181
  },
132
182
  sessions: {
133
183
  total: stats.total,
@@ -213,17 +263,21 @@ class MCPHTTPServer {
213
263
 
214
264
  if (isRequest) {
215
265
  // Handle JSON-RPC request
216
- const response = await this.handleRequest(message, session, sessionId);
266
+ const response = await this.handleRequest(message, session, sessionId, req.userToken);
217
267
 
218
268
  // If this is an initialize request, create session and include session ID
219
269
  if (message.method === 'initialize' && response.result) {
220
270
  sessionId = this.sessionManager.createSession();
221
271
  this.sessionManager.initializeSession(sessionId, message.params?.capabilities);
222
272
 
273
+ // Create a per-session API client bound to this user's token
274
+ const sessionApiClient = createApiClient(req.userToken);
275
+ this.sessionManager.setApiClient(sessionId, sessionApiClient);
276
+
223
277
  // Set session ID header in response
224
278
  res.setHeader('Mcp-Session-Id', sessionId);
225
279
 
226
- console.error(`Session initialized: ${sessionId}`);
280
+ console.error(`Session initialized: ${sessionId} (per-user token)`);
227
281
  }
228
282
 
229
283
  // Check if we should stream the response via SSE
@@ -377,19 +431,24 @@ class MCPHTTPServer {
377
431
  /**
378
432
  * Handle JSON-RPC request
379
433
  */
380
- async handleRequest(message, session, sessionId) {
434
+ async handleRequest(message, session, sessionId, userToken) {
381
435
  // Create MCP server instance for this request
382
436
  const mcpServer = new Server({
383
437
  name: process.env.MCP_SERVER_NAME || 'planning-tools',
384
- version: process.env.MCP_SERVER_VERSION || '0.3.1'
438
+ version: process.env.MCP_SERVER_VERSION || version
385
439
  }, {
386
440
  capabilities: {
387
441
  tools: {}
388
442
  }
389
443
  });
390
444
 
391
- // Setup tools on the server
392
- setupTools(mcpServer);
445
+ // Get per-session API client (bound to user's token), or create one for initialize requests
446
+ const sessionApiClient = session
447
+ ? this.sessionManager.getApiClient(sessionId)
448
+ : (userToken ? createApiClient(userToken) : null);
449
+
450
+ // Setup tools with the per-session API client
451
+ setupTools(mcpServer, sessionApiClient);
393
452
 
394
453
  // Process the request through MCP server
395
454
  try {
@@ -88,6 +88,28 @@ class SessionManager {
88
88
  console.error(`Session initialized: ${sessionId}`);
89
89
  }
90
90
 
91
+ /**
92
+ * Store a per-session API client
93
+ * @param {string} sessionId - Session ID
94
+ * @param {Object} apiClient - API client instance bound to user's token
95
+ */
96
+ setApiClient(sessionId, apiClient) {
97
+ const session = this.sessions.get(sessionId);
98
+ if (session) {
99
+ session.apiClient = apiClient;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Get the per-session API client
105
+ * @param {string} sessionId - Session ID
106
+ * @returns {Object|null} API client or null
107
+ */
108
+ getApiClient(sessionId) {
109
+ const session = this.sessions.get(sessionId);
110
+ return session?.apiClient || null;
111
+ }
112
+
91
113
  /**
92
114
  * Check if a session is initialized
93
115
  * @param {string} sessionId - Session ID
package/src/setup.js CHANGED
@@ -124,7 +124,7 @@ function createEnvFile(config) {
124
124
  API_URL=${config.apiUrl}
125
125
  USER_API_TOKEN=${config.token}
126
126
  MCP_SERVER_NAME=planning-system
127
- MCP_SERVER_VERSION=0.2.0
127
+ MCP_SERVER_VERSION=${require('../package.json').version}
128
128
  NODE_ENV=production
129
129
  `;
130
130