agent-planner-mcp 0.5.0 → 0.5.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/AGENT_GUIDE.md +257 -0
- package/README.md +128 -286
- package/SKILL.md +438 -0
- package/package.json +17 -6
- package/src/api-client.js +298 -66
- package/src/index.js +3 -2
- package/src/server-http.js +73 -14
- package/src/session-manager.js +22 -0
- package/src/setup.js +1 -1
- package/src/tools.js +653 -210
- package/claude-code/AUTONOMOUS_EXECUTION_GUIDE.md +0 -335
- package/claude-code/commands/README.md +0 -112
- package/claude-code/commands/create-plan.md +0 -174
- package/claude-code/commands/execute-plan.md +0 -202
- package/claude-code/commands/plan-status.md +0 -145
- package/claude-code/settings.template.json +0 -12
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
|
-
|
|
563
|
-
|
|
564
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
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 ||
|
|
75
|
+
version: process.env.MCP_SERVER_VERSION || version
|
|
75
76
|
}, {
|
|
76
77
|
capabilities: {
|
|
77
78
|
tools: {}
|
package/src/server-http.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
//
|
|
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 ||
|
|
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 ||
|
|
438
|
+
version: process.env.MCP_SERVER_VERSION || version
|
|
385
439
|
}, {
|
|
386
440
|
capabilities: {
|
|
387
441
|
tools: {}
|
|
388
442
|
}
|
|
389
443
|
});
|
|
390
444
|
|
|
391
|
-
//
|
|
392
|
-
|
|
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 {
|
package/src/session-manager.js
CHANGED
|
@@ -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
|
|
127
|
+
MCP_SERVER_VERSION=${require('../package.json').version}
|
|
128
128
|
NODE_ENV=production
|
|
129
129
|
`;
|
|
130
130
|
|